feat: let the global chat create a resource type after the user confirms it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-10-08 14:32:17 +02:00
co-authored by Claude Opus 5.5
parent 7e04d7d982
commit 8f8987bd97
5 changed files with 55 additions and 6 deletions
@@ -41,6 +41,7 @@ import type { TokenUsage, ToolCallDetail } from "../shared/types";
const MUTATING_GLOBAL_TOOLS = new Set([
"deploy_workspace_item",
"delete_workspace_item",
"create_resource_type",
]);
const DISABLE_ACTIVE_EDITOR_CONTEXT_ENV =
"WMILL_AI_EVAL_DISABLE_ACTIVE_EDITOR_CONTEXT";
+7 -2
View File
@@ -761,7 +761,8 @@
# The instance never synced the hub's resource types, so the one Confluence takes is missing:
# the script's connection picker stays empty until a superadmin syncs, which only the
# assistant's reply can tell the user.
# assistant's reply can tell the user. A type of its own would duplicate the hub's once synced,
# so it is only for a user who says they would rather not sync.
- id: global-resource-type-hub-not-synced
prompt: |-
Write a script that creates a page in our Confluence space from a space key, a title and the page body.
@@ -777,6 +778,7 @@
toolExpect:
forbiddenToolsUsed:
- write_variable
- create_resource_type
- deploy_workspace_item
assistantExpect:
requiredMentionsAnyOf:
@@ -786,7 +788,8 @@
- the script creates a Confluence page from a space key, a title and a body
# An in-house API has no hub resource type, so its connection needs a type of the workspace's
# own: a token parameter or a variable read inside the code is the fallback the guidance rules out.
# own, created once the user confirms its schema: a token parameter or a variable read inside
# the code is the fallback the guidance rules out.
- id: global-resource-type-custom-api
prompt: |-
Write a script that fetches an invoice by id from our internal billing API at https://billing.acme.internal/api/v1. It authenticates with a bearer token.
@@ -801,6 +804,8 @@
- getVariable
- get_variable
toolExpect:
requiredToolsUsed:
- create_resource_type
forbiddenToolsUsed:
- write_variable
- deploy_workspace_item
@@ -721,6 +721,20 @@ const searchResourceTypesSchema = z.object({
.describe('Max number of resource types to return. Defaults to 5.')
})
const createResourceTypeSchema = z.object({
name: z
.string()
.describe(
'Type name, 1 to 50 letters, digits, "_" or "-", e.g. acme_billing. Resources and resource-typed parameters refer to it by this name.'
),
description: z.string().describe('What a resource of this type connects to.'),
schema: z
.record(z.string(), z.any())
.describe(
'JSON Schema of a resource value of this type: { "type": "object", "properties": {...}, "required": [...] }, with every connection field, secrets included (a resource fills those with "$var:" references).'
)
})
const getRunSchema = z.object({
id: z.string().describe('The UUID of the run (job) to inspect.'),
step: z
@@ -1404,8 +1418,10 @@ const buildGlobalSystemPrompt = (
// rule cannot outlive the tool it describes. An unresolved profile keeps every block.
const canWriteDraft = !access || access.has('write_draft')
const canRunPreview = !access || access.has('run_preview')
// The deploy tools take their kind as an argument, so `deploy` gates only the folder text.
// The deploy tools take their kind as an argument, so `deploy` gates only the folder and
// resource type text.
const canCreateFolder = !access || access.has('deploy')
const canCreateResourceType = !access || access.has('deploy')
// Each gated block carries its own leading newline, so dropping one leaves no blank
// line behind and a full-access prompt is byte-for-byte the ungated text.
const when = (cond: boolean, block: string) => (cond ? block : '')
@@ -1488,7 +1504,7 @@ Rules:${when(
- Hub scripts are prebuilt, vetted integrations for third-party services, hosted outside the workspace under \`hub/<version>/<app>/<name>\` paths. Check search_hub_scripts before hand-writing code against a third-party API, even when the user never mentions the hub; read a result with read_workspace_item type "script" and its hub path to get its code, language, and input schema. Use what you find in whichever way fits: reference the hub path directly from a flow module or app runnable when a script already does the job, copy it into a workspace draft and adapt it when it is close (note the source hub path in a comment at the top of the code), or take it as a worked example and write your own. A script that does not do what the user asked is still worth reading when it is the only example of that integration: pass its \`integration\` back to search_hub_scripts to list that integration's other scripts with their descriptions, or use the \`suggested_integrations\` a search hands back when it finds nothing.
- Before writing your own code against an integration the hub covers, call get_hub_integration with its slug: it returns the resource type to take, its auth fields and the integration's most-used scripts, which beats inferring them from script bodies. Call it for the integration you are about to write against, whichever it is. A search marks an integration \`documented\` when the hub additionally holds provider knowledge checked against the live API — pagination, enums, error codes and gotchas — so read that closely where it appears rather than trusting your own memory of the API.
- If you have a web search tool and the hub does not cover a third-party API, search for the vendor's own API documentation rather than writing its endpoints and auth from memory, and link the page you relied on. Reserve it for external APIs: search_docs answers questions about Windmill itself.
${when(canRunPreview, '- Use get_db_schema with a database resource path to fetch its tables and columns before writing SQL (or a script querying that database).\n')}- Use get_instructions before writing scripts, flows, resources, or apps. For scripts, pass the target language.
${when(canCreateResourceType, '- create_resource_type creates a resource type the workspace lacks. The user confirms the schema in the call itself, so that is where you propose it rather than asking first in chat.\n')}${when(canRunPreview, '- Use get_db_schema with a database resource path to fetch its tables and columns before writing SQL (or a script querying that database).\n')}- Use get_instructions before writing scripts, flows, resources, or apps. For scripts, pass the target language.
${pipelineBullet}`
)}${when(
canRunPreview && canWriteDraft,
@@ -4294,6 +4310,33 @@ export const globalTools: SessionTool<{}>[] = [
return JSON.stringify(note ? { resource_types: resourceTypes, note } : resourceTypes, null, 2)
}
},
{
// Not a draft: resource types have no draft kind, so this writes the workspace once the
// user confirms, under the same right as a deploy.
requires: DEPLOY,
def: createToolDef(
createResourceTypeSchema,
'create_resource_type',
'Create a resource type in the workspace. Mutates the workspace once the user confirms the schema.',
{ strict: false }
),
showDetails: true,
requiresConfirmation: true,
confirmationMessage: 'Create resource type',
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
const parsed = createResourceTypeSchema.parse(args)
await ResourceService.createResourceType({
workspace,
requestBody: {
name: parsed.name,
description: parsed.description,
schema: parsed.schema
}
})
toolCallbacks.setToolStatus(toolId, { content: `Created resource type ${parsed.name}` })
return JSON.stringify({ success: true, message: `Created resource type ${parsed.name}` })
}
},
createDbSchemaTool<{}>({
description:
'Fetch the schema (tables and columns) of a database resource by its path. Supports postgresql, mysql, ms_sql_server, snowflake and bigquery resources.',
+1 -1
View File
@@ -834,7 +834,7 @@ The Windmill Hub publishes resource types for most services, and an instance get
- If looking up resource types reports that the instance is missing the hub's types, mention that a superadmin can sync them: the hub most likely has one for the service. Create one of your own only if the user still wants it, since after a sync the workspace would hold both.
- Otherwise, or when the user prefers it to syncing, propose a resource type holding the service's connection fields (base URL, account or region identifiers, credentials), as in "Custom Resource Types" below, and ask the user before creating it: every resource of that type and every script taking one depends on its fields.
- You cannot create a resource type yourself: give the user the schema to add with **Add resource type** on the Resources page.
- Where you have no way to create a resource type yourself, give the user the schema to add with **Add resource type** on the Resources page.
- A plain variable instead of a resource is fine when the user asks for it; without that, default to the resource, its secret in a variable it references.
## Custom Resource Types
+1 -1
View File
@@ -220,7 +220,7 @@ The Windmill Hub publishes resource types for most services, and an instance get
- If looking up resource types reports that the instance is missing the hub's types, mention that a superadmin can sync them: the hub most likely has one for the service. Create one of your own only if the user still wants it, since after a sync the workspace would hold both.
- Otherwise, or when the user prefers it to syncing, propose a resource type holding the service's connection fields (base URL, account or region identifiers, credentials), as in "Custom Resource Types" below, and ask the user before creating it: every resource of that type and every script taking one depends on its fields.
<!-- chat-only -->
- You cannot create a resource type yourself: give the user the schema to add with **Add resource type** on the Resources page.
- Where you have no way to create a resource type yourself, give the user the schema to add with **Add resource type** on the Resources page.
<!-- /chat-only -->
- A plain variable instead of a resource is fine when the user asks for it; without that, default to the resource, its secret in a variable it references.