From dffb89e00632bd4e7bbe9998bccb1f14357d9c07 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 22 Apr 2026 16:46:21 +0200 Subject: [PATCH 01/28] fix: trigger failure_module when branchone predicate throws (#8905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A BranchOne predicate expression that threw was propagated as a flow-level error, bypassing the flow's failure_module — especially silent when nested inside a forloop with skip_failures: true, where the failed iteration was swallowed with no handler ever invoked. Catch the predicate-eval error inside compute_next_flow_transform's BranchOne case, return a new NextFlowTransform::StepFailure variant, and have push_next_flow_job route it through update_flow_status_after_job_completion with success=false. Predicate errors now behave exactly like a failing script step: failure_module runs when defined, skip_failures still skips, workspace error handler fires when the flow fails. Closes #8889 --- backend/windmill-worker/src/worker_flow.rs | 44 +++++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 7b9289c56a..6b759a575f 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -53,7 +53,7 @@ use windmill_common::runnable_settings::{ use windmill_common::scripts::{ScriptHash, ScriptRunnableSettingsInline}; use windmill_common::users::username_to_permissioned_as; use windmill_common::utils::WarnAfterExt; -use windmill_common::worker::{to_raw_value, Connection}; +use windmill_common::worker::{error_to_value, to_raw_value, Connection}; use windmill_common::{ add_time, get_latest_flow_version_info_for_path, get_script_info_for_hash, FlowVersionInfo, ScriptHashInfo, DB, @@ -3540,6 +3540,32 @@ async fn push_next_flow_job( )); } } + NextFlowTransform::StepFailure { error } => { + let result = Arc::new(to_raw_value(&WrappedError { error })); + update_flow_status_after_job_completion( + db, + client, + flow_job.id, + &Uuid::nil(), + &flow_job.workspace_id, + false, + None, + result, + None, + false, + same_worker_tx, + worker_dir, + None, + worker_name, + job_completed_tx, + flow_runners, + killpill_rx, + #[cfg(feature = "benchmark")] + &mut BenchmarkIter::new(), + ) + .await?; + return Ok(PushNextFlowJob::Done(None)); + } }; // only start runners if we're not already in a squash for loop @@ -4396,6 +4422,10 @@ enum ContinuePayload { enum NextFlowTransform { EmptyInnerFlows { branch_chosen: Option }, Continue(ContinuePayload, NextStatus), + // The current module failed in-place (e.g. a BranchOne predicate threw). + // The error is reported as the step's result so the normal flow machinery + // (including `failure_module` and surrounding `skip_failures`) applies. + StepFailure { error: serde_json::Value }, } fn insert_iter_arg( @@ -4793,6 +4823,7 @@ async fn compute_next_flow_transform( | FlowStatusModule::WaitingForExecutor { .. } => { let mut branch_chosen = BranchChosen::Default; let idcontext = get_transform_context(&flow_job, previous_id, &status); + let mut predicate_err: Option = None; for (i, b) in branches.iter().enumerate() { let pred_res = compute_bool_from_expr( &b.expr, @@ -4818,13 +4849,22 @@ async fn compute_next_flow_transform( ) .await; } - let pred = pred_res?; + let pred = match pred_res { + Ok(p) => p, + Err(e) => { + predicate_err = Some(e); + break; + } + }; if pred { branch_chosen = BranchChosen::Branch { branch: i }; break; } } + if let Some(e) = predicate_err { + return Ok(NextFlowTransform::StepFailure { error: error_to_value(&e) }); + } branch_chosen } _ => Err(Error::BadRequest(format!( From 932d18331196ef3e87c45d8a06ae45ac8013bd7a Mon Sep 17 00:00:00 2001 From: Guilhem Date: Wed, 22 Apr 2026 16:46:40 +0200 Subject: [PATCH 02/28] fix: persist flow groups from AI chat tool calls (#8906) * fix: persist flow groups from AI chat tool calls Co-Authored-By: Claude Opus 4.7 (1M context) * fix: validate group ids and coerce empty groups to undefined Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../copilot/chat/flow/FlowAIChat.svelte | 8 +- .../lib/components/copilot/chat/flow/core.ts | 107 ++++++++---- .../copilot/chat/flow/helperUtils.test.ts | 161 +++++++++++++++++- .../copilot/chat/flow/helperUtils.ts | 50 +++++- 4 files changed, 281 insertions(+), 45 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte index 4e8dd3cc85..d8ca5858ea 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIChat.svelte @@ -177,13 +177,14 @@ return { errorCount: 0, warningCount: 0, errors: [], warnings: [] } }, - setFlowJson: async ({ modules, schema, preprocessorModule, failureModule }) => { + setFlowJson: async ({ modules, schema, preprocessorModule, failureModule, groups }) => { try { if ( modules !== undefined || schema !== undefined || preprocessorModule !== undefined || - failureModule !== undefined + failureModule !== undefined || + groups !== undefined ) { // Take snapshot of current flowStore and set as beforeFlow if (!diffManager?.hasPendingChanges) { @@ -197,7 +198,8 @@ modules, schema, preprocessorModule, - failureModule + failureModule, + groups }) // Refresh the state store to update UI diff --git a/frontend/src/lib/components/copilot/chat/flow/core.ts b/frontend/src/lib/components/copilot/chat/flow/core.ts index 23065085fd..9a877ff07e 100644 --- a/frontend/src/lib/components/copilot/chat/flow/core.ts +++ b/frontend/src/lib/components/copilot/chat/flow/core.ts @@ -35,7 +35,7 @@ import type { ContextElement } from '../context' import type { ExtendedOpenFlow } from '$lib/components/flows/types' import { findModuleInFlow, findModuleInModules } from '$lib/components/flows/flowTree' import { createInlineScriptSession, type InlineScriptSession } from './inlineScriptsUtils' -import type { FlowJsonUpdateResult } from './helperUtils' +import { validateFlowGroups, type FlowGroup, type FlowJsonUpdateResult } from './helperUtils' import { flowModuleSchema, flowModulesSchema } from './openFlowZod' import { collectAllFlowModuleIdsFromModules } from '$lib/components/flows/flowTree' import { FLOW_CHAT_SPECIAL_MODULES, getFlowPrompt } from '$system_prompts' @@ -268,6 +268,7 @@ type FlowJsonUpdate = { schema?: Record | null preprocessorModule?: FlowModule | null failureModule?: FlowModule | null + groups?: FlowGroup[] | null } type EditableFlowJson = { @@ -275,6 +276,7 @@ type EditableFlowJson = { schema: Record | null preprocessor_module: FlowModule | null failure_module: FlowModule | null + groups: FlowGroup[] | null } function formatEmptyInlineScriptWarning({ @@ -386,12 +388,19 @@ function validateEditableFlowJson(rawFlow: unknown): EditableFlowJson { 'preprocessor_module' ) const failureModule = validateOptionalFlowModule(flow.failure_module, 'failure_module') + const groupModuleIds = new Set(collectAllFlowModuleIdsFromModules(modules)) + const groups = validateFlowGroups(flow.groups, groupModuleIds) if (preprocessorModule) { if (preprocessorModule.id !== SPECIAL_MODULE_IDS.PREPROCESSOR) { - throw new Error(`Invalid preprocessor_module: id must be "${SPECIAL_MODULE_IDS.PREPROCESSOR}"`) + throw new Error( + `Invalid preprocessor_module: id must be "${SPECIAL_MODULE_IDS.PREPROCESSOR}"` + ) } - if (preprocessorModule.value.type !== 'rawscript' && preprocessorModule.value.type !== 'script') { + if ( + preprocessorModule.value.type !== 'rawscript' && + preprocessorModule.value.type !== 'script' + ) { throw new Error( 'Invalid preprocessor_module: only "rawscript" and "script" modules are supported' ) @@ -402,9 +411,7 @@ function validateEditableFlowJson(rawFlow: unknown): EditableFlowJson { throw new Error(`Invalid failure_module: id must be "${SPECIAL_MODULE_IDS.FAILURE}"`) } if (failureModule.value.type !== 'rawscript' && failureModule.value.type !== 'script') { - throw new Error( - 'Invalid failure_module: only "rawscript" and "script" modules are supported' - ) + throw new Error('Invalid failure_module: only "rawscript" and "script" modules are supported') } } @@ -423,7 +430,8 @@ function validateEditableFlowJson(rawFlow: unknown): EditableFlowJson { modules, schema, preprocessor_module: preprocessorModule, - failure_module: failureModule + failure_module: failureModule, + groups } } @@ -455,7 +463,11 @@ function buildEditableFlowJson( } let failureModule = flow.value.failure_module - if (failureModule?.value?.type === 'rawscript' && failureModule.value.content && inlineScriptSession) { + if ( + failureModule?.value?.type === 'rawscript' && + failureModule.value.content && + inlineScriptSession + ) { inlineScriptSession.set(failureModule.id, failureModule.value.content) failureModule = { ...failureModule, @@ -470,7 +482,8 @@ function buildEditableFlowJson( modules, schema: flow.schema ?? null, preprocessor_module: preprocessorModule ?? null, - failure_module: failureModule ?? null + failure_module: failureModule ?? null, + groups: flow.value.groups ?? null } } @@ -563,13 +576,20 @@ const setFlowJsonToolSchema = z.object({ .string() .optional() .nullable() - .describe('JSON string containing the optional failure module') + .describe('JSON string containing the optional failure module'), + groups: z + .string() + .optional() + .nullable() + .describe( + 'JSON string containing the optional array of semantic flow groups (summary, note, autocollapse, start_id, end_id, color). Pass null to clear groups.' + ) }) const setFlowJsonToolDef = createToolDef( setFlowJsonToolSchema, 'set_flow_json', - 'Set the complete flow modules array and optionally the flow input schema, preprocessor module, and failure module.', + 'Set the complete flow modules array and optionally the flow input schema, preprocessor module, failure module, and semantic groups.', { strict: false } ) @@ -644,10 +664,7 @@ function validateSpecialFlowModule( } const patchFlowJsonSchema = z.object({ - old_string: z - .string() - .min(1) - .describe('Exact text to find in the current compact flow JSON'), + old_string: z.string().min(1).describe('Exact text to find in the current compact flow JSON'), new_string: z.string().describe('Replacement JSON text'), replace_all: z .boolean() @@ -864,13 +881,13 @@ export const flowTools: Tool[] = [ // Test script step - need to get the script content const script = moduleValue.hash ? await ScriptService.getScriptByHash({ - workspace: workspace, - hash: moduleValue.hash - }) + workspace: workspace, + hash: moduleValue.hash + }) : await ScriptService.getScriptByPath({ - workspace: workspace, - path: moduleValue.path - }) + workspace: workspace, + path: moduleValue.path + }) return executeTestRun({ jobStarter: () => @@ -1023,7 +1040,8 @@ export const flowTools: Tool[] = [ modules: parsedFlow.modules, schema: parsedFlow.schema, preprocessorModule: parsedFlow.preprocessor_module, - failureModule: parsedFlow.failure_module + failureModule: parsedFlow.failure_module, + groups: parsedFlow.groups }) const warning = formatEmptyInlineScriptWarning(updateResult) @@ -1058,7 +1076,9 @@ export const flowTools: Tool[] = [ toolCallbacks.setToolStatus(toolId, { content: - parsedModule === null ? 'Removing preprocessor module...' : 'Setting preprocessor module...' + parsedModule === null + ? 'Removing preprocessor module...' + : 'Setting preprocessor module...' }) const updateResult = await helpers.setFlowJson({ preprocessorModule: parsedModule }) const warning = formatEmptyInlineScriptWarning(updateResult) @@ -1073,7 +1093,8 @@ export const flowTools: Tool[] = [ } toolCallbacks.setToolStatus(toolId, { - content: parsedModule === null ? 'Preprocessor module removed' : 'Preprocessor module updated', + content: + parsedModule === null ? 'Preprocessor module removed' : 'Preprocessor module updated', result: 'Success' }) return parsedModule === null @@ -1123,16 +1144,20 @@ export const flowTools: Tool[] = [ showDetails: true, showFade: true, fn: async ({ args, helpers, toolId, toolCallbacks }) => { - const { modules, schema, preprocessor_module, failure_module } = args + const { modules, schema, preprocessor_module, failure_module, groups } = args let parsedModules: FlowModule[] | null | undefined let parsedSchema: Record | null | undefined let parsedPreprocessorModule: FlowModule | null | undefined let parsedFailureModule: FlowModule | null | undefined + let parsedGroups: FlowGroup[] | null | undefined // Parse JSON strings parsedModules = parseOptionalJsonArg(modules, 'modules') as FlowModule[] | null | undefined - parsedSchema = parseOptionalJsonArg(schema, 'schema') as Record | null | undefined + parsedSchema = parseOptionalJsonArg(schema, 'schema') as + | Record + | null + | undefined parsedPreprocessorModule = parseOptionalJsonArg( preprocessor_module, 'preprocessor_module' @@ -1141,6 +1166,7 @@ export const flowTools: Tool[] = [ | FlowModule | null | undefined + parsedGroups = parseOptionalJsonArg(groups, 'groups') as FlowGroup[] | null | undefined if (parsedModules === null) { parsedModules = undefined } @@ -1151,8 +1177,7 @@ export const flowTools: Tool[] = [ if (parsedModules !== undefined) { parsedModules = validateFlowModules(parsedModules) const reservedIds = collectAllFlowModuleIdsFromModules(parsedModules).filter( - (id) => - id === SPECIAL_MODULE_IDS.PREPROCESSOR || id === SPECIAL_MODULE_IDS.FAILURE + (id) => id === SPECIAL_MODULE_IDS.PREPROCESSOR || id === SPECIAL_MODULE_IDS.FAILURE ) if (reservedIds.length > 0) { throw new Error( @@ -1170,12 +1195,18 @@ export const flowTools: Tool[] = [ ) parsedFailureModule = validateSpecialFlowModule(parsedFailureModule, 'failure_module') + if (parsedGroups !== undefined) { + const effectiveModules = + parsedModules ?? helpers.getFlowAndSelectedId().flow.value.modules ?? [] + const moduleIdsForGroups = new Set(collectAllFlowModuleIdsFromModules(effectiveModules)) + parsedGroups = validateFlowGroups(parsedGroups, moduleIdsForGroups) + } + const ids = [ ...(parsedModules ? collectAllFlowModuleIdsFromModules(parsedModules) : []), - ...([parsedPreprocessorModule, parsedFailureModule].filter( - (module): module is FlowModule => module !== undefined && module !== null - ) - .map((module) => module.id)) + ...[parsedPreprocessorModule, parsedFailureModule] + .filter((module): module is FlowModule => module !== undefined && module !== null) + .map((module) => module.id) ] if (ids.length !== new Set(ids).size) { throw new Error('Duplicate module IDs found in flow') @@ -1190,7 +1221,8 @@ export const flowTools: Tool[] = [ ...(parsedPreprocessorModule !== undefined ? { preprocessorModule: parsedPreprocessorModule } : {}), - ...(parsedFailureModule !== undefined ? { failureModule: parsedFailureModule } : {}) + ...(parsedFailureModule !== undefined ? { failureModule: parsedFailureModule } : {}), + ...(parsedGroups !== undefined ? { groups: parsedGroups } : {}) }) const warning = formatEmptyInlineScriptWarning(updateResult) @@ -1203,9 +1235,9 @@ export const flowTools: Tool[] = [ const { selectedId } = helpers.getFlowAndSelectedId() const selectedModule = selectedId === SPECIAL_MODULE_IDS.PREPROCESSOR - ? parsedPreprocessorModule ?? undefined + ? (parsedPreprocessorModule ?? undefined) : selectedId === SPECIAL_MODULE_IDS.FAILURE - ? parsedFailureModule ?? undefined + ? (parsedFailureModule ?? undefined) : parsedModules ? findModuleInModules(parsedModules, selectedId) : undefined @@ -1290,7 +1322,7 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS Use \`patch_flow_json\` for small, localized changes when you can target an exact snippet from the \`CURRENT FLOW JSON COMPACT\` block below. Always copy the exact search text from the \`CURRENT FLOW JSON COMPACT\` block below. -The compact JSON is a single object with \`modules\`, \`schema\`, \`preprocessor_module\`, and \`failure_module\` keys. +The compact JSON is a single object with \`modules\`, \`schema\`, \`preprocessor_module\`, \`failure_module\`, and \`groups\` keys. **Parameters:** - \`old_string\`: Exact JSON text to find @@ -1311,13 +1343,14 @@ ${FLOW_CHAT_SPECIAL_MODULES} ## Flow Modification with set_flow_json -Use the \`set_flow_json\` tool to set the entire flow structure at once. Provide the complete modules array and optionally the flow input schema, \`preprocessor_module\`, and \`failure_module\`. +Use the \`set_flow_json\` tool to set the entire flow structure at once. Provide the complete modules array and optionally the flow input schema, \`preprocessor_module\`, \`failure_module\`, and \`groups\`. **Parameters:** - \`modules\`: Array of flow modules (required) - \`schema\`: Flow input schema in JSON Schema format (optional) - \`preprocessor_module\`: Special module that runs before \`modules\` (optional, separate from \`modules\`) - \`failure_module\`: Special module that runs on failure (optional, separate from \`modules\`) +- \`groups\`: Array of semantic groups for organizing modules in the editor (optional). Each group has \`summary\` (display name), \`note\` (markdown description shown below the group header — attached directly to the group, not a separate sticky note), \`autocollapse\`, \`start_id\`, \`end_id\`, and \`color\`. \`start_id\` and \`end_id\` must reference existing module IDs in the flow (not \`preprocessor\` or \`failure\`). Groups do not affect execution — they provide naming and collapsibility in the editor. Pass \`null\` to clear existing groups. **Example - Simple flow:** \`\`\`javascript diff --git a/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts b/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts index dc8234e361..f17abdd901 100644 --- a/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts +++ b/frontend/src/lib/components/copilot/chat/flow/helperUtils.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from 'vitest' import type { FlowModule } from '$lib/gen' -import { applyFlowJsonUpdate, updateRawScriptModuleContent } from './helperUtils' +import { + applyFlowJsonUpdate, + updateRawScriptModuleContent, + validateFlowGroups +} from './helperUtils' import { createInlineScriptSession } from './inlineScriptsUtils' vi.mock('../shared', () => ({ @@ -63,7 +67,9 @@ describe('applyFlowJsonUpdate', () => { makeRawScriptModule('validate_data', 'inline_script.validate_data') ] }) - const [processDataModule, validateDataModule] = flow.value.modules as Array + const [processDataModule, validateDataModule] = flow.value.modules as Array< + FlowModule & { value: any } + > expect(result.emptyInlineScriptModuleIds).toEqual(['validate_data']) expect(inlineScriptSession.has('validate_data')).toBe(false) @@ -85,7 +91,7 @@ describe('applyFlowJsonUpdate', () => { applyFlowJsonUpdate(flow as any, inlineScriptSession, { modules: [makeRawScriptModule('validate_data', 'inline_script.other_module')] }) - ).toThrow('Unresolved inline script references: other_module') + ).toThrow('Unresolved inline script references: other_module') }) it('keeps the inline script session unchanged after a failed update so retries still warn', () => { @@ -124,6 +130,98 @@ describe('applyFlowJsonUpdate', () => { expect(inlineScriptSession.has('validate_data')).toBe(false) }) + it('persists groups passed in the flow json update', () => { + const flow = { + value: { + modules: [ + makeRawScriptModule('fetch_data', 'existing code'), + makeRawScriptModule('process_data', 'existing code') + ] + } + } + const inlineScriptSession = createInlineScriptSession() + inlineScriptSession.set('fetch_data', 'existing code') + inlineScriptSession.set('process_data', 'existing code') + + applyFlowJsonUpdate(flow as any, inlineScriptSession, { + groups: [ + { + summary: 'Data Ingestion', + note: 'Fetches and processes data', + start_id: 'fetch_data', + end_id: 'process_data' + } + ] + }) + + expect((flow.value as any).groups).toEqual([ + { + summary: 'Data Ingestion', + note: 'Fetches and processes data', + start_id: 'fetch_data', + end_id: 'process_data' + } + ]) + }) + + it('clears groups when an empty array is passed', () => { + const flow = { + value: { + modules: [], + groups: [ + { + summary: 'existing', + start_id: 'a', + end_id: 'b' + } + ] + } + } + const inlineScriptSession = createInlineScriptSession() + + applyFlowJsonUpdate(flow as any, inlineScriptSession, { groups: [] }) + + expect((flow.value as any).groups).toBeUndefined() + }) + + it('clears groups when null is passed', () => { + const flow = { + value: { + modules: [], + groups: [ + { + summary: 'existing', + start_id: 'a', + end_id: 'b' + } + ] + } + } + const inlineScriptSession = createInlineScriptSession() + + applyFlowJsonUpdate(flow as any, inlineScriptSession, { groups: null }) + + expect((flow.value as any).groups).toBeUndefined() + }) + + it('leaves groups untouched when not provided in the update', () => { + const existingGroups = [{ summary: 'existing', start_id: 'a', end_id: 'b' }] + const flow = { + value: { + modules: [makeRawScriptModule('a', 'existing code')], + groups: existingGroups + } + } + const inlineScriptSession = createInlineScriptSession() + inlineScriptSession.set('a', 'existing code') + + applyFlowJsonUpdate(flow as any, inlineScriptSession, { + modules: [makeRawScriptModule('a', 'inline_script.a')] + }) + + expect((flow.value as any).groups).toEqual(existingGroups) + }) + it('updates ai agent rawscript tools in place when changing module code', () => { const flow = { value: { @@ -145,3 +243,60 @@ describe('applyFlowJsonUpdate', () => { ) }) }) + +describe('validateFlowGroups', () => { + it('returns null for null input', () => { + expect(validateFlowGroups(null)).toBeNull() + expect(validateFlowGroups(undefined)).toBeNull() + }) + + it('rejects non-array input', () => { + expect(() => validateFlowGroups({})).toThrow('Flow groups must be an array') + expect(() => validateFlowGroups('not an array')).toThrow('Flow groups must be an array') + }) + + it('rejects a group that is not an object', () => { + expect(() => validateFlowGroups(['nope'])).toThrow( + 'Invalid group at index 0: must be an object' + ) + }) + + it('rejects a group with a missing or non-string start_id', () => { + expect(() => validateFlowGroups([{ end_id: 'b' }])).toThrow( + 'Invalid group at index 0: start_id must be a non-empty string' + ) + expect(() => validateFlowGroups([{ start_id: '', end_id: 'b' }])).toThrow( + 'Invalid group at index 0: start_id must be a non-empty string' + ) + expect(() => validateFlowGroups([{ start_id: 42, end_id: 'b' }])).toThrow( + 'Invalid group at index 0: start_id must be a non-empty string' + ) + }) + + it('rejects a group with a missing end_id', () => { + expect(() => validateFlowGroups([{ start_id: 'a' }])).toThrow( + 'Invalid group at index 0: end_id must be a non-empty string' + ) + }) + + it('accepts a valid group with no moduleIds set', () => { + const result = validateFlowGroups([{ summary: 'G', start_id: 'a', end_id: 'b' }]) + expect(result).toEqual([{ summary: 'G', start_id: 'a', end_id: 'b' }]) + }) + + it('rejects start_id or end_id that are not in the moduleIds set', () => { + const moduleIds = new Set(['a', 'b']) + expect(() => validateFlowGroups([{ start_id: 'missing', end_id: 'b' }], moduleIds)).toThrow( + 'Invalid group at index 0: start_id "missing" does not match any flow module' + ) + expect(() => validateFlowGroups([{ start_id: 'a', end_id: 'missing' }], moduleIds)).toThrow( + 'Invalid group at index 0: end_id "missing" does not match any flow module' + ) + }) + + it('accepts groups whose ids are all in the moduleIds set', () => { + const moduleIds = new Set(['a', 'b', 'c']) + const result = validateFlowGroups([{ start_id: 'a', end_id: 'c', summary: 'G' }], moduleIds) + expect(result).toEqual([{ start_id: 'a', end_id: 'c', summary: 'G' }]) + }) +}) diff --git a/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts b/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts index 19f2e984a7..45c7c7d5fe 100644 --- a/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts +++ b/frontend/src/lib/components/copilot/chat/flow/helperUtils.ts @@ -1,4 +1,4 @@ -import type { FlowModule, OpenFlow, RawScript } from '$lib/gen' +import type { FlowModule, FlowValue, OpenFlow, RawScript } from '$lib/gen' import { forEachFlowModule } from '$lib/components/flows/dfs' import { findModuleInFlow } from '$lib/components/flows/flowTree' import type { InlineScriptSession } from './inlineScriptsUtils' @@ -7,11 +7,14 @@ type FlowLike = Pick & { schema?: Record } +export type FlowGroup = NonNullable[number] + export interface FlowJsonUpdate { modules?: FlowModule[] schema?: Record | null preprocessorModule?: FlowModule | null failureModule?: FlowModule | null + groups?: FlowGroup[] | null } export interface FlowJsonUpdateResult { @@ -33,10 +36,49 @@ export function updateRawScriptModuleContent( return rawScriptModule } +export function validateFlowGroups( + rawGroups: unknown, + moduleIds?: Set +): FlowGroup[] | null { + if (rawGroups == null) { + return null + } + + if (!Array.isArray(rawGroups)) { + throw new Error('Flow groups must be an array') + } + + return rawGroups.map((group, index) => { + if (!group || typeof group !== 'object' || Array.isArray(group)) { + throw new Error(`Invalid group at index ${index}: must be an object`) + } + const g = group as Record + if (typeof g.start_id !== 'string' || !g.start_id) { + throw new Error(`Invalid group at index ${index}: start_id must be a non-empty string`) + } + if (typeof g.end_id !== 'string' || !g.end_id) { + throw new Error(`Invalid group at index ${index}: end_id must be a non-empty string`) + } + if (moduleIds) { + if (!moduleIds.has(g.start_id)) { + throw new Error( + `Invalid group at index ${index}: start_id "${g.start_id}" does not match any flow module` + ) + } + if (!moduleIds.has(g.end_id)) { + throw new Error( + `Invalid group at index ${index}: end_id "${g.end_id}" does not match any flow module` + ) + } + } + return g as unknown as FlowGroup + }) +} + export function applyFlowJsonUpdate( flow: FlowLike, inlineScriptSession: InlineScriptSession, - { modules, schema, preprocessorModule, failureModule }: FlowJsonUpdate + { modules, schema, preprocessorModule, failureModule, groups }: FlowJsonUpdate ): FlowJsonUpdateResult { const emptyInlineScriptModuleIds = new Set() @@ -66,6 +108,10 @@ export function applyFlowJsonUpdate( : restoreFlowModule(failureModule, inlineScriptSession, emptyInlineScriptModuleIds) } + if (groups !== undefined) { + flow.value.groups = groups == null || groups.length === 0 ? undefined : groups + } + return { emptyInlineScriptModuleIds: Array.from(emptyInlineScriptModuleIds) } From f29badcf368e7c712f1515fa30a6a0e179a4bdc5 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 07:50:14 -0700 Subject: [PATCH 03/28] fix: push parent resource on fileset child add/delete (#8910) Co-authored-by: Claude Opus 4.7 (1M context) --- cli/src/commands/sync/sync.ts | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 668e21b82b..ad7a895fe8 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -3397,12 +3397,48 @@ export async function push( await writeFile(stateTarget, change.after, "utf-8"); } } else if (change.name === "added") { + if (isFilesetResource(change.path)) { + // Re-push the parent resource so the new fileset file is included. + // If the parent is also being added here, its own push covers all children. + let resourceFilePath: string | undefined; + try { + resourceFilePath = await findFilesetResourceFile(change.path); + } catch { + continue; + } + if (alreadySynced.includes(resourceFilePath)) { + continue; + } + alreadySynced.push(resourceFilePath); + + const newObj = parseFromPath( + resourceFilePath, + await readFile(resourceFilePath, "utf-8"), + ); + + let serverPath = resourceFilePath; + const currentBranch = cachedWsNameForPush; + if (currentBranch && isWorkspaceSpecificFile(resourceFilePath)) { + serverPath = fromWorkspaceSpecificPath( + resourceFilePath, + currentBranch, + ); + } + + await pushResource( + workspace.workspaceId, + serverPath, + undefined, + newObj, + resourceFilePath, + ); + continue; + } if ( change.path.endsWith(".script.json") || change.path.endsWith(".script.yaml") || change.path.endsWith(".lock") || - isFileResource(change.path) || - isFilesetResource(change.path) + isFileResource(change.path) ) { continue; } else if ( @@ -3484,6 +3520,44 @@ export async function push( ); continue; } + if (isFilesetResource(change.path)) { + // Re-push the parent resource with the updated fileset contents. + // If the parent is also being deleted, its own "deleted" change + // removes the whole resource, so skip this child. + let resourceFilePath: string | undefined; + try { + resourceFilePath = await findFilesetResourceFile(change.path); + } catch { + continue; + } + if (alreadySynced.includes(resourceFilePath)) { + continue; + } + alreadySynced.push(resourceFilePath); + + const newObj = parseFromPath( + resourceFilePath, + await readFile(resourceFilePath, "utf-8"), + ); + + let serverPath = resourceFilePath; + const currentBranch = cachedWsNameForPush; + if (currentBranch && isWorkspaceSpecificFile(resourceFilePath)) { + serverPath = fromWorkspaceSpecificPath( + resourceFilePath, + currentBranch, + ); + } + + await pushResource( + workspace.workspaceId, + serverPath, + undefined, + newObj, + resourceFilePath, + ); + continue; + } const typ = getTypeStrFromPath(change.path); if (typ == "script") { From dc896737ac1dcd90ab96314b2bc2f044ff833b8a Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 08:44:00 -0700 Subject: [PATCH 04/28] fix: apply powershell workspace dependencies to deployed scripts (#8912) * fix: persist powershell workspace deps in deployed script lock Co-Authored-By: Claude Opus 4.5 * fix: trigger dep job for powershell scripts on deploy Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- backend/windmill-api-scripts/src/scripts.rs | 1 + backend/windmill-worker/src/pwsh_executor.rs | 27 ++++++++++++++++--- .../windmill-worker/src/worker_lockfiles.rs | 1 + 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index c4b928bcb5..35ca6c5282 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -883,6 +883,7 @@ async fn create_script_internal<'c>( || ns.language == ScriptLang::Java || ns.language == ScriptLang::Ruby || ns.language == ScriptLang::Rlang + || ns.language == ScriptLang::Powershell // for related places search: ADD_NEW_LANG ) { Some(String::new()) diff --git a/backend/windmill-worker/src/pwsh_executor.rs b/backend/windmill-worker/src/pwsh_executor.rs index 9924913ad0..b5b8444f56 100644 --- a/backend/windmill-worker/src/pwsh_executor.rs +++ b/backend/windmill-worker/src/pwsh_executor.rs @@ -6,7 +6,9 @@ use sqlx::types::Json; use tokio::process::Command; use windmill_common::client::AuthedClient; use windmill_common::error::Error; +use windmill_common::scripts::ScriptLang; use windmill_common::worker::{to_raw_value, write_file, Connection}; +use windmill_common::workspace_dependencies::clean_lock_from_annotations; use windmill_queue::{ append_logs, CanceledBy, MiniPulledJob, INIT_SCRIPT_PATH_PREFIX, PERIODIC_SCRIPT_PATH_PREFIX, }; @@ -417,10 +419,16 @@ pub async fn handle_powershell_job( // Resolve modules from workspace dependencies and/or script imports let all_modules = match &maybe_lock { MaybeLock::Resolved { lock } if !lock.is_empty() => { - // Deployed script with lock: parse workspace deps from lock, merge with script imports - let ws_modules = parse_modules_json(lock)?; + // Deployed script with lock: strip workspace-dependencies annotation header, + // parse the modules.json body, and merge with script imports. + let cleaned = clean_lock_from_annotations(lock, ScriptLang::Powershell); let script_modules = parse_script_imports(content); - merge_module_requests(ws_modules, script_modules) + if cleaned.trim().is_empty() { + script_modules + } else { + let ws_modules = parse_modules_json(&cleaned)?; + merge_module_requests(ws_modules, script_modules) + } } MaybeLock::Unresolved { workspace_dependencies } => { let script_modules = parse_script_imports(content); @@ -1070,6 +1078,19 @@ mod tests { assert!(parse_modules_json("not json").is_err()); } + #[test] + fn test_parse_modules_json_after_cleaning_header() { + // Simulates the deployed-script lock: workspace-dependencies header + // prepended to the modules.json content. `clean_lock_from_annotations` + // strips header lines so the remaining body can be JSON-parsed. + let lock = "# workspace-dependencies-mode: manual\n# workspace-dependencies: default:abc123\n{\"modules\": {\"PSWriteColor\": \"1.0.0\"}}"; + let cleaned = clean_lock_from_annotations(lock, ScriptLang::Powershell); + let modules = parse_modules_json(&cleaned).unwrap(); + assert_eq!(modules.len(), 1); + assert_eq!(modules[0].name, "PSWriteColor"); + assert_eq!(modules[0].version, Some("1.0.0".to_string())); + } + // --- parse_script_imports tests --- #[test] diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 4275bb7014..a6b062b337 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -2881,6 +2881,7 @@ async fn capture_dependency_job( ) .await? } + ScriptLang::Powershell => workspace_dependencies.get_powershell()?.unwrap_or_default(), // for related places search: ADD_NEW_LANG _ => "".to_owned(), }; From 99bc96d0b231a2af303b28aa87d5de9141ee5cab Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 08:50:47 -0700 Subject: [PATCH 05/28] feat: auto-strip UTF-8 BOM when reading local files in CLI (#8911) Co-authored-by: Claude Opus 4.5 --- cli/src/commands/app/app_metadata.ts | 8 +-- cli/src/commands/app/bundle.ts | 9 +-- cli/src/commands/app/dev.ts | 6 +- cli/src/commands/app/raw_apps.ts | 17 +++-- cli/src/commands/dependencies/dependencies.ts | 3 +- cli/src/commands/dev/dev.ts | 7 +- cli/src/commands/flow/flow.ts | 7 +- cli/src/commands/flow/flow_metadata.ts | 7 +- cli/src/commands/instance/instance.ts | 8 +-- cli/src/commands/jobs/jobs.ts | 5 +- cli/src/commands/resource/resource.ts | 6 +- cli/src/commands/script/script.ts | 18 ++--- cli/src/commands/sync/sync.ts | 11 +-- cli/src/commands/workspace/workspace.ts | 7 +- cli/src/core/branch-profiles.ts | 5 +- cli/src/guidance/writer.ts | 9 +-- cli/src/types.ts | 7 +- cli/src/utils/local_path_scripts.ts | 7 +- cli/src/utils/metadata.ts | 22 +++--- cli/src/utils/utils.ts | 48 ++++++++++++- cli/src/utils/yaml.ts | 4 +- cli/test/utils_unit.test.ts | 70 ++++++++++++++++++- 22 files changed, 204 insertions(+), 87 deletions(-) diff --git a/cli/src/commands/app/app_metadata.ts b/cli/src/commands/app/app_metadata.ts index 37965a6c60..2589bf2fca 100644 --- a/cli/src/commands/app/app_metadata.ts +++ b/cli/src/commands/app/app_metadata.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { readFile, mkdir, readdir } from "node:fs/promises"; +import { mkdir, readdir } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; import * as log from "../../core/log.ts"; import { sep as SEP } from "node:path"; @@ -23,7 +23,7 @@ import { ScriptLanguage, workspaceDependenciesLanguages, } from "../../utils/script_common.ts"; -import { generateHash, getHeaders, writeIfChanged } from "../../utils/utils.ts"; +import { generateHash, getHeaders, readTextFile, writeIfChanged } from "../../utils/utils.ts"; import { exts } from "../script/script.ts"; import { FSFSElement, yamlOptions } from "../sync/sync.ts"; import { Workspace } from "../workspace/workspace.ts"; @@ -178,7 +178,7 @@ export async function generateAppLocksInternal( if (typeof content === "string" && content.startsWith("!inline ")) { const filePath = appFolder + SEP + content.replace("!inline ", ""); try { - content = await readFile(filePath, "utf-8"); + content = await readTextFile(filePath); } catch { return inlineScript; } @@ -893,7 +893,7 @@ export async function inferRunnableSchemaFromFile( ); let content: string; try { - content = await readFile(fullFilePath, "utf-8"); + content = await readTextFile(fullFilePath); } catch { log.warn(colors.yellow(`Could not read file: ${fullFilePath}`)); return undefined; diff --git a/cli/src/commands/app/bundle.ts b/cli/src/commands/app/bundle.ts index 8d15488491..0f82f7b8c4 100644 --- a/cli/src/commands/app/bundle.ts +++ b/cli/src/commands/app/bundle.ts @@ -5,6 +5,7 @@ import { spawn } from "node:child_process"; import * as log from "../../core/log.ts"; import { colors } from "@cliffy/ansi/colors"; import * as windmillUtils from "@windmill-labs/shared-utils"; +import { readTextFile, readTextFileSync } from "../../utils/utils.ts"; export interface BundleOptions { entryPoint?: string; outDir?: string; @@ -41,7 +42,7 @@ export function detectFrameworks(appDir: string): { svelte: boolean; vue: boolea } try { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); + const packageJson = JSON.parse(readTextFileSync(packageJsonPath)); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies, @@ -69,7 +70,7 @@ function createSveltePlugin(appDir: string): any { const svelte = await import("svelte/compiler"); // Load the file from the file system - const source = await fs.promises.readFile(args.path, "utf8"); + const source = await readTextFile(args.path); const filename = path.relative(process.cwd(), args.path); // This converts a message in Svelte's format to esbuild's format @@ -269,9 +270,9 @@ export async function createBundle( throw new Error(`Expected JS bundle at ${jsPath} but file not found`); } - const jsContent = fs.readFileSync(jsPath, "utf-8"); + const jsContent = readTextFileSync(jsPath); const cssContent = fs.existsSync(cssPath) - ? fs.readFileSync(cssPath, "utf-8") + ? readTextFileSync(cssPath) : ""; try { diff --git a/cli/src/commands/app/dev.ts b/cli/src/commands/app/dev.ts index c9fe8f67ec..628849c375 100644 --- a/cli/src/commands/app/dev.ts +++ b/cli/src/commands/app/dev.ts @@ -13,7 +13,7 @@ import * as path from "node:path"; import process from "node:process"; import { Buffer } from "node:buffer"; import { writeFileSync } from "node:fs"; -import { readFile } from "node:fs/promises"; +import { readTextFile } from "../../utils/utils.ts"; import { WebSocket, WebSocketServer } from "ws"; import { createFrameworkPlugins, @@ -800,7 +800,7 @@ async function dev(opts: DevOptions, appFolder?: string) { const fileName = path.basename(filePath); try { - const sqlContent = await readFile(filePath, "utf-8"); + const sqlContent = await readTextFile(filePath); if (!sqlContent.trim()) { log.info(colors.gray(`Skipping empty file: ${fileName}`)); @@ -856,7 +856,7 @@ async function dev(opts: DevOptions, appFolder?: string) { // If there's a current SQL file being shown, send it to the new client if (currentSqlFile && fs.existsSync(currentSqlFile)) { try { - const sqlContent = await readFile(currentSqlFile, "utf-8"); + const sqlContent = await readTextFile(currentSqlFile); const datatable = await getDatatableConfig(); const fileName = path.basename(currentSqlFile); diff --git a/cli/src/commands/app/raw_apps.ts b/cli/src/commands/app/raw_apps.ts index 6e71a3bf30..3cd5837e4d 100644 --- a/cli/src/commands/app/raw_apps.ts +++ b/cli/src/commands/app/raw_apps.ts @@ -9,10 +9,10 @@ import { stringify as yamlStringify } from "yaml"; import * as wmill from "../../../gen/services.gen.ts"; import { Policy } from "../../../gen/types.gen.ts"; import path from "node:path"; -import { readFile, readdir } from "node:fs/promises"; +import { readdir } from "node:fs/promises"; import { GlobalOptions, isSuperset } from "../../types.ts"; -import { deepEqual } from "../../utils/utils.ts"; +import { deepEqual, readTextFile } from "../../utils/utils.ts"; import { replaceInlineScripts, repopulateFields } from "./app.ts"; import { createBundle, detectFrameworks } from "./bundle.ts"; @@ -65,8 +65,8 @@ async function findRunnableContentFile( // Check if this is a recognized extension if (EXTENSION_TO_LANGUAGE[ext]) { try { - const content = await readFile( - path.join(backendPath, fileName), "utf-8", + const content = await readTextFile( + path.join(backendPath, fileName), ); return { ext, content }; } catch { @@ -164,9 +164,8 @@ export async function loadRunnablesFromBackend( // Try to load lock file let lock: string | undefined; try { - lock = await readFile( + lock = await readTextFile( path.join(backendPath, `${runnableId}.lock`), - "utf-8", ); } catch { // No lock file, that's fine @@ -226,8 +225,8 @@ export async function loadRunnablesFromBackend( // Try to load lock file let lock: string | undefined; try { - lock = await readFile( - path.join(backendPath, `${runnableId}.lock`), "utf-8", + lock = await readTextFile( + path.join(backendPath, `${runnableId}.lock`), ); } catch { // No lock file, that's fine @@ -319,7 +318,7 @@ async function collectAppFiles( ) { continue; } - const content = await readFile(fullPath, "utf-8"); + const content = await readTextFile(fullPath); files[relativePath] = content; } } diff --git a/cli/src/commands/dependencies/dependencies.ts b/cli/src/commands/dependencies/dependencies.ts index cde4fe256c..2daf2d180a 100644 --- a/cli/src/commands/dependencies/dependencies.ts +++ b/cli/src/commands/dependencies/dependencies.ts @@ -7,6 +7,7 @@ import * as log from "../../core/log.ts"; import * as wmill from "../../../gen/services.gen.ts"; import fs from "node:fs"; import { workspaceDependenciesPathToLanguageAndFilename } from "../../utils/metadata.ts"; +import { readTextFileSync } from "../../utils/utils.ts"; async function push( opts: GlobalOptions, @@ -19,7 +20,7 @@ async function push( throw new Error(`File not found: ${filePath}`); } - const content = fs.readFileSync(filePath, "utf8"); + const content = readTextFileSync(filePath); // Use the existing pushWorkspaceDependencies function await pushWorkspaceDependencies( diff --git a/cli/src/commands/dev/dev.ts b/cli/src/commands/dev/dev.ts index 5352270f15..b49aad2073 100644 --- a/cli/src/commands/dev/dev.ts +++ b/cli/src/commands/dev/dev.ts @@ -7,7 +7,8 @@ import { WebSocket, WebSocketServer } from "ws"; import * as getPort from "get-port"; import * as http from "node:http"; import * as open from "open"; -import { readFile, realpath } from "node:fs/promises"; +import { realpath } from "node:fs/promises"; +import { readTextFile } from "../../utils/utils.ts"; import { watch } from "node:fs"; import { getTypeStrFromPath, GlobalOptions } from "../../types.ts"; import { ignoreF } from "../sync/sync.ts"; @@ -93,7 +94,7 @@ async function dev(opts: GlobalOptions & SyncOptions) { )) as FlowFile; await replaceInlineScripts( localFlow.value.modules, - async (path: string) => await readFile(localPath + path, "utf-8"), + async (path: string) => await readTextFile(localPath + path), log, localPath, SEP, @@ -114,7 +115,7 @@ async function dev(opts: GlobalOptions & SyncOptions) { log.info("Updated " + localPath); broadcastChanges(currentLastEdit); } else if (typ == "script") { - const content = await readFile(cpath, "utf-8"); + const content = await readTextFile(cpath); const splitted = cpath.split("."); const wmPath = splitted[0]; const lang = inferContentTypeFromFilePath(cpath, opts.defaultTs); diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index a613aa2b0f..0fa59c84ea 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -7,9 +7,8 @@ import * as log from "../../core/log.ts"; import { sep as SEP } from "node:path"; import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; -import { validateRequiredArgs } from "../../utils/utils.ts"; +import { readTextFile, validateRequiredArgs } from "../../utils/utils.ts"; import * as wmill from "../../../gen/services.gen.ts"; -import { readFile } from "node:fs/promises"; import { mkdirSync, writeFileSync } from "node:fs"; import { buildFolderPath, getMetadataFileName, loadNonDottedPathsSetting } from "../../utils/resource_folders.ts"; @@ -157,7 +156,7 @@ export async function pushFlow( } const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile; - const fileReader = async (path: string) => await readFile(localPath + path, "utf-8"); + const fileReader = async (path: string) => await readTextFile(localPath + path); const missingFiles: string[] = []; await replaceInlineScripts( localFlow.value.modules, @@ -545,7 +544,7 @@ async function preview( const localFlow = (await yamlParseFile(flowPath + "flow.yaml")) as FlowFile; // Replace inline scripts with their actual content - const fileReader = async (path: string) => await readFile(flowPath + path, "utf-8"); + const fileReader = async (path: string) => await readTextFile(flowPath + path); await replaceInlineScripts( localFlow.value.modules, fileReader, diff --git a/cli/src/commands/flow/flow_metadata.ts b/cli/src/commands/flow/flow_metadata.ts index 3c1c8cbd15..4110004959 100644 --- a/cli/src/commands/flow/flow_metadata.ts +++ b/cli/src/commands/flow/flow_metadata.ts @@ -4,7 +4,6 @@ import * as path from "node:path"; import { sep as SEP } from "node:path"; import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; -import { readFile } from "node:fs/promises"; import { GlobalOptions } from "../../types.ts"; import { readLockfile, @@ -21,7 +20,7 @@ import { ScriptLanguage } from "../../utils/script_common.ts"; import { extractInlineScripts as extractInlineScriptsForFlows, extractCurrentMapping } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts"; import { newPathAssigner } from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts"; -import { generateHash, getHeaders, writeIfChanged } from "../../utils/utils.ts"; +import { generateHash, getHeaders, readTextFile, writeIfChanged } from "../../utils/utils.ts"; import { exts } from "../script/script.ts"; import { FSFSElement, yamlOptions } from "../sync/sync.ts"; import { Workspace } from "../workspace/workspace.ts"; @@ -109,7 +108,7 @@ export async function generateFlowLockInternal( if (content.startsWith("!inline ")) { const filePath = folder + SEP + content.replace("!inline ", ""); try { - content = await readFile(filePath, "utf-8"); + content = await readTextFile(filePath); } catch { continue; } @@ -192,7 +191,7 @@ export async function generateFlowLockInternal( if (!noStaleMessage) { log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`); } - const fileReader = async (path: string) => await readFile(folder + SEP + path, "utf-8"); + const fileReader = async (path: string) => await readTextFile(folder + SEP + path); // Capture existing module-ID-to-file-path mapping before replaceInlineScripts // overwrites the !inline references with actual file content. This preserves diff --git a/cli/src/commands/instance/instance.ts b/cli/src/commands/instance/instance.ts index 3f21189074..6289dd2b01 100644 --- a/cli/src/commands/instance/instance.ts +++ b/cli/src/commands/instance/instance.ts @@ -1,4 +1,4 @@ -import { readFile, writeFile, readdir, mkdir, rm, stat } from "node:fs/promises"; +import { writeFile, readdir, mkdir, rm, stat } from "node:fs/promises"; import { appendFile } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; @@ -39,7 +39,7 @@ import { pushInstanceSettings, type SimplifiedSettings, } from "../../core/settings.ts"; -import { deepEqual } from "../../utils/utils.ts"; +import { deepEqual, readTextFile } from "../../utils/utils.ts"; import { getActiveWorkspace } from "../workspace/workspace.ts"; export interface Instance { @@ -52,7 +52,7 @@ export interface Instance { export async function allInstances(): Promise { try { const file = await getInstancesConfigFilePath(); - const txt = await readFile(file, "utf-8"); + const txt = await readTextFile(file); return txt .split("\n") .map((line) => { @@ -658,7 +658,7 @@ export async function getActiveInstance(opts: { return opts.instance; } try { - return await readFile(await getActiveInstanceFilePath(), "utf-8"); + return await readTextFile(await getActiveInstanceFilePath()); } catch { return undefined; } diff --git a/cli/src/commands/jobs/jobs.ts b/cli/src/commands/jobs/jobs.ts index 17a58d11f2..e7b421ab9d 100644 --- a/cli/src/commands/jobs/jobs.ts +++ b/cli/src/commands/jobs/jobs.ts @@ -7,6 +7,7 @@ import { Confirm } from "@cliffy/prompt/confirm"; import * as log from "../../core/log.ts"; import { mergeConfigWithConfigFile } from "../../core/conf.ts"; import * as fs from "node:fs/promises"; +import { readTextFile } from "../../utils/utils.ts"; import * as wmill from "../../../gen/services.gen.ts"; async function pullJobs( @@ -190,7 +191,7 @@ async function pushJobs( // Push completed jobs const completedPath = opts.completedFile || "completed_jobs.json"; try { - const completedContent = await fs.readFile(completedPath, "utf-8"); + const completedContent = await readTextFile(completedPath); const completedJobs = JSON.parse(completedContent); if (!Array.isArray(completedJobs)) { @@ -218,7 +219,7 @@ async function pushJobs( // Push queued jobs const queuedPath = opts.queuedFile || "queued_jobs.json"; try { - const queuedContent = await fs.readFile(queuedPath, "utf-8"); + const queuedContent = await readTextFile(queuedPath); const queuedJobs = JSON.parse(queuedContent); if (!Array.isArray(queuedJobs)) { diff --git a/cli/src/commands/resource/resource.ts b/cli/src/commands/resource/resource.ts index 054ed91d06..f8a8713928 100644 --- a/cli/src/commands/resource/resource.ts +++ b/cli/src/commands/resource/resource.ts @@ -1,4 +1,4 @@ -import { mkdir, stat, writeFile, readdir, readFile } from "node:fs/promises"; +import { mkdir, stat, writeFile, readdir } from "node:fs/promises"; import { stringify as yamlStringify } from "yaml"; import nodePath from "node:path"; @@ -17,7 +17,7 @@ import * as log from "../../core/log.ts"; import { sep as SEP } from "node:path"; import * as wmill from "../../../gen/services.gen.ts"; import { Resource } from "../../../gen/types.gen.ts"; -import { readInlinePathSync } from "../../utils/utils.ts"; +import { readInlinePathSync, readTextFile } from "../../utils/utils.ts"; import { isWorkspaceSpecificFile } from "../../core/specific_items.ts"; import { getCurrentGitBranch } from "../../utils/git.ts"; @@ -38,7 +38,7 @@ async function readFilesetDirectory(dirPath: string): Promise { const remotePath = removeExtensionToPath(filePath).replaceAll(SEP, "/"); const metadataWithType = await parseMetadataFile(remotePath, undefined); - const metadataContent = await readFile(metadataWithType.path, "utf-8"); + const metadataContent = await readTextFile(metadataWithType.path); return await generateScriptHash({}, content, metadataContent); } @@ -141,7 +141,7 @@ async function push(opts: PushOptions, filePath: string) { // Warn about metadata state before pushing try { - const content = await readFile(filePath, "utf-8"); + const content = await readTextFile(filePath); const remotePath = removeExtensionToPath(filePath).replaceAll(SEP, "/"); const contentHash = await computePushMetadataHash(filePath, content); const conf = await readLockfile(); @@ -432,7 +432,7 @@ export async function handleFile( } catch { log.debug(`Script ${remotePath} does not exist on remote`); } - const content = await readFile(path, "utf-8"); + const content = await readTextFile(path); if (opts?.skipScriptsMetadata) { // if (codebase) { @@ -619,7 +619,7 @@ export async function readModulesFromDisk( } else if (entry.isFile() && !entry.name.endsWith(".lock") && !isEntryPointFile(entry.name, isTopLevel)) { // Skip lock files — they're handled as the `lock` field on ScriptModule if (exts.some((ext) => entry.name.endsWith(ext))) { - const content = fs.readFileSync(fullPath, "utf-8"); + const content = readTextFileSync(fullPath); const language = inferContentTypeFromFilePath(entry.name, defaultTs); // Check for an accompanying lock file (helper.lock) @@ -627,7 +627,7 @@ export async function readModulesFromDisk( const lockPath = path.join(dirPath, baseName + ".lock"); let lock: string | undefined; if (fs.existsSync(lockPath)) { - lock = fs.readFileSync(lockPath, "utf-8"); + lock = readTextFileSync(lockPath); } modules[relPath] = { @@ -958,7 +958,7 @@ export async function resolve(input: string): Promise> { input = new TextDecoder().decode(Buffer.concat(chunks)); } if (input[0] == "@") { - input = await readFile(input.substring(1), "utf-8"); + input = await readTextFile(input.substring(1)); } try { return JSON.parse(input); @@ -1404,7 +1404,7 @@ async function preview( const codebases = await listSyncCodebases(opts); const language = inferContentTypeFromFilePath(filePath, opts?.defaultTs); - const content = await readFile(filePath, "utf-8"); + const content = await readTextFile(filePath); const input = opts.data ? await resolve(opts.data) : {}; // Read modules from __mod/ folder if present diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index ad7a895fe8..b08afb86e2 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -1,6 +1,6 @@ import { requireLogin } from "../../core/auth.ts"; import { fetchVersion, resolveWorkspace } from "../../core/context.ts"; -import { readFile, writeFile, readdir, stat, rm, copyFile, mkdir } from "node:fs/promises"; +import { writeFile, readdir, stat, rm, copyFile, mkdir } from "node:fs/promises"; import { colors } from "@cliffy/ansi/colors"; import { Command } from "@cliffy/command"; import { Confirm } from "@cliffy/prompt/confirm"; @@ -42,6 +42,7 @@ import { isFilesetResource, isRawAppFile, isWorkspaceDependencies, + readTextFile, } from "../../utils/utils.ts"; import { getEffectiveSettings, @@ -325,7 +326,7 @@ export async function FSFSElement( } }, async getContentText(): Promise { - const content = await readFile(localP, "utf-8"); + const content = await readTextFile(localP); const itemPath = localP.substring(p.length + 1); const r = await addCodebaseDigestIfRelevant( itemPath, @@ -2331,7 +2332,7 @@ export async function pull( if (change.name === "edited") { if (opts.stateful) { try { - const currentLocal = await readFile(target, "utf-8"); + const currentLocal = await readTextFile(target); if ( currentLocal !== change.before && currentLocal !== change.after @@ -3307,7 +3308,7 @@ export async function push( const newObj = parseFromPath( resourceFilePath, - await readFile(resourceFilePath, "utf-8"), + await readTextFile(resourceFilePath), ); // For branch-specific resources, push to the base path on the workspace server @@ -3342,7 +3343,7 @@ export async function push( const newObj = parseFromPath( resourceFilePath, - await readFile(resourceFilePath, "utf-8"), + await readTextFile(resourceFilePath), ); let serverPath = resourceFilePath; diff --git a/cli/src/commands/workspace/workspace.ts b/cli/src/commands/workspace/workspace.ts index d2bc2a5bbc..d8cc16b01b 100644 --- a/cli/src/commands/workspace/workspace.ts +++ b/cli/src/commands/workspace/workspace.ts @@ -1,4 +1,5 @@ -import { readFile, writeFile, open as fsOpen } from "node:fs/promises"; +import { writeFile, open as fsOpen } from "node:fs/promises"; +import { readTextFile } from "../../utils/utils.ts"; import process from "node:process"; import { GlobalOptions } from "../../types.ts"; import { @@ -31,7 +32,7 @@ export async function allWorkspaces( ): Promise { try { const file = await getWorkspaceConfigFilePath(configDirOverride); - const txt = await readFile(file, "utf-8"); + const txt = await readTextFile(file); return txt .split("\n") .map((line) => { @@ -55,7 +56,7 @@ async function getActiveWorkspaceName( } try { const file = await getActiveWorkspaceConfigFilePath(opts?.configDir); - return await readFile(file, "utf-8"); + return await readTextFile(file); } catch { return undefined; } diff --git a/cli/src/core/branch-profiles.ts b/cli/src/core/branch-profiles.ts index 80c72b0705..bacbcc51aa 100644 --- a/cli/src/core/branch-profiles.ts +++ b/cli/src/core/branch-profiles.ts @@ -1,5 +1,6 @@ import * as log from "./log.ts"; -import { readFile, writeFile } from "node:fs/promises"; +import { writeFile } from "node:fs/promises"; +import { readTextFile } from "../utils/utils.ts"; import { getStore } from "./store.ts"; export interface BranchProfileMapping { @@ -17,7 +18,7 @@ export async function getBranchProfilesPath(configDirOverride?: string): Promise export async function loadBranchProfiles(configDirOverride?: string): Promise { try { const path = await getBranchProfilesPath(configDirOverride); - const content = await readFile(path, "utf-8"); + const content = await readTextFile(path); return JSON.parse(content); } catch { // File doesn't exist or invalid JSON - return empty mapping diff --git a/cli/src/guidance/writer.ts b/cli/src/guidance/writer.ts index 0acab52784..b301272882 100644 --- a/cli/src/guidance/writer.ts +++ b/cli/src/guidance/writer.ts @@ -1,4 +1,5 @@ -import { cp, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import { cp, mkdir, readdir, stat, writeFile } from "node:fs/promises"; +import { readTextFile } from "../utils/utils.ts"; import { join } from "node:path"; import { generateAgentsMdContent } from "./core.ts"; import { @@ -47,7 +48,7 @@ export async function writeAiGuidanceFiles( overwrite: options.overwriteProjectGuidance ?? false, content: options.agentsSourcePath != null - ? await readFile(options.agentsSourcePath, "utf8") + ? await readTextFile(options.agentsSourcePath) : generateAgentsMdContent(buildSkillsReference(skillMetadata)), }); @@ -56,7 +57,7 @@ export async function writeAiGuidanceFiles( overwrite: options.overwriteProjectGuidance ?? false, content: options.claudeSourcePath != null - ? await readFile(options.claudeSourcePath, "utf8") + ? await readTextFile(options.claudeSourcePath) : CLAUDE_MD_DEFAULT, }); @@ -202,7 +203,7 @@ async function readSkillMetadataFromDirectory(skillsDir: string): Promise { try { - return await readFile(scriptPath + ".script.lock", "utf-8"); + return await readTextFile(scriptPath + ".script.lock"); } catch { return undefined; } @@ -138,7 +139,7 @@ export async function resolvePreviewLocalScriptState( return { filePath, - content: await readFile(filePath, "utf-8"), + content: await readTextFile(filePath), language, lock: normalizeOptionalLock(rawLock), tag: metadata?.payload?.tag, diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 26e050d5b2..e4032d4857 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -4,7 +4,7 @@ import { colors } from "@cliffy/ansi/colors"; import * as log from "../core/log.ts"; import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "./yaml.ts"; -import { readFile, writeFile, stat, rm, readdir } from "node:fs/promises"; +import { writeFile, stat, rm, readdir } from "node:fs/promises"; import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, writeFileSync } from "node:fs"; import * as path from "node:path"; import { createRequire } from "node:module"; @@ -21,7 +21,7 @@ import { import { inferContentTypeFromFilePath } from "./script_common.ts"; import { getModuleFolderSuffix, isModuleEntryPoint, getScriptBasePathFromModulePath } from "./resource_folders.ts"; import { findCodebase, yamlOptions } from "../commands/sync/sync.ts"; -import { generateHash, readInlinePathSync, getHeaders } from "./utils.ts"; +import { generateHash, readInlinePathSync, getHeaders, readTextFile, readTextFileSync } from "./utils.ts"; import { SyncCodebase } from "./codebase.ts"; import { argSigToJsonSchemaType } from "../../windmill-utils-internal/src/parse/parse-schema.ts"; @@ -66,7 +66,7 @@ export async function getRawWorkspaceDependencies(legacyBehaviour: boolean): Pro if (entry.isDirectory()) continue; const filePath = `dependencies/${entry.name}`; - const content = await readFile(filePath, "utf-8"); + const content = await readTextFile(filePath); // Find matching language for (const lang of workspaceDependenciesLanguages) { @@ -153,7 +153,7 @@ export async function filterWorkspaceDependenciesForScripts( if (content.startsWith("!inline ")) { const filePath = folder + sep + content.replace("!inline ", ""); try { - content = await readFile(filePath, "utf-8"); + content = await readTextFile(filePath); } catch { continue; } @@ -212,8 +212,8 @@ export async function generateScriptMetadataInternal( ); // read script content - const scriptContent = await readFile(scriptPath, "utf-8"); - const metadataContent = await readFile(metadataWithType.path, "utf-8"); + const scriptContent = await readTextFile(scriptPath); + const metadataContent = await readTextFile(metadataWithType.path); const filteredRawWorkspaceDependencies = filterWorkspaceDependencies( rawWorkspaceDependencies, @@ -744,7 +744,7 @@ async function updateModuleLocks( if (!changedModules.includes(normalizedRelPath)) continue; } - const moduleContent = readFileSync(fullPath, "utf-8"); + const moduleContent = readTextFileSync(fullPath); const moduleRemotePath = scriptRemotePath + "/" + relPath; log.debug(`Generating lock for module ${relPath}`); @@ -986,7 +986,7 @@ export async function parseMetadataFileIfExists( let metadataFilePath = scriptPath + ".script.json"; try { await stat(metadataFilePath); - const payload = JSON.parse(await readFile(metadataFilePath, "utf-8")); + const payload = JSON.parse(await readTextFile(metadataFilePath)); replaceLock(payload); return { path: metadataFilePath, @@ -1028,7 +1028,7 @@ export async function parseMetadataFile( await stat(metadataFilePath); return { path: metadataFilePath, - payload: JSON.parse(await readFile(metadataFilePath, "utf-8")), + payload: JSON.parse(await readTextFile(metadataFilePath)), isJson: true, }; } catch { @@ -1051,7 +1051,7 @@ export async function parseMetadataFile( await stat(metadataFilePath); return { path: metadataFilePath, - payload: JSON.parse(await readFile(metadataFilePath, "utf-8")), + payload: JSON.parse(await readTextFile(metadataFilePath)), isJson: true, }; } catch { @@ -1229,7 +1229,7 @@ async function computeModuleHashes( } catch { continue; } - const content = readFileSync(fullPath, "utf-8"); + const content = readTextFileSync(fullPath); const normalizedPath = normalizeLockPath(relPath); hashes[normalizedPath] = await generateHash( content + JSON.stringify(rawWorkspaceDependencies) diff --git a/cli/src/utils/utils.ts b/cli/src/utils/utils.ts index eb8babfff3..ff9fe9b78c 100644 --- a/cli/src/utils/utils.ts +++ b/cli/src/utils/utils.ts @@ -131,9 +131,53 @@ export async function generateHashFromBuffer( return Buffer.from(hashBuffer).toString("hex"); } +function decodeBufferAsUtf8(buf: Buffer, path: string | URL): string { + if (buf.length >= 2) { + if (buf[0] === 0xff && buf[1] === 0xfe) { + if (buf.length >= 4 && buf[2] === 0x00 && buf[3] === 0x00) { + throw new Error( + `File ${path} is encoded as UTF-32 LE, which is not supported. Please convert it to UTF-8.` + ); + } + throw new Error( + `File ${path} is encoded as UTF-16 LE, which is not supported. Please convert it to UTF-8.` + ); + } + if (buf[0] === 0xfe && buf[1] === 0xff) { + throw new Error( + `File ${path} is encoded as UTF-16 BE, which is not supported. Please convert it to UTF-8.` + ); + } + if (buf.length >= 4 && buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0xfe && buf[3] === 0xff) { + throw new Error( + `File ${path} is encoded as UTF-32 BE, which is not supported. Please convert it to UTF-8.` + ); + } + } + if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) { + return buf.subarray(3).toString("utf-8"); + } + return buf.toString("utf-8"); +} + +export function stripBom(content: string): string { + if (content.charCodeAt(0) === 0xfeff) { + return content.slice(1); + } + return content; +} + +export async function readTextFile(path: string | URL): Promise { + return decodeBufferAsUtf8(await readFile(path), path); +} + +export function readTextFileSync(path: string | URL): string { + return decodeBufferAsUtf8(readFileSync(path), path); +} + export function readInlinePathSync(path: string): string { try { - return readFileSync(path.replaceAll("/", SEP), "utf-8"); + return readTextFileSync(path.replaceAll("/", SEP)); } catch (error) { log.warn(`Error reading inline path: ${path}, ${error}`); return ""; @@ -253,7 +297,7 @@ export async function getIsWin(): Promise { */ export function writeIfChanged(path: string, content: string): boolean { try { - const existing = readFileSync(path, "utf-8"); + const existing = readTextFileSync(path); if (existing === content) { return false; // Content unchanged, skip write } diff --git a/cli/src/utils/yaml.ts b/cli/src/utils/yaml.ts index 52ec682067..566537d58e 100644 --- a/cli/src/utils/yaml.ts +++ b/cli/src/utils/yaml.ts @@ -1,6 +1,6 @@ import { parse as yamlParse } from "yaml"; import type { ParseOptions, DocumentOptions, SchemaOptions, ToJSOptions, ScalarTag } from "yaml"; -import { readFile } from "node:fs/promises"; +import { readTextFile } from "./utils.ts"; // Custom YAML tags that resolve `!inline value` and `!inline_fileset value` // back to their string-prefix form ("!inline value"). @@ -26,7 +26,7 @@ type YamlParseOptions = ParseOptions & DocumentOptions & SchemaOptions & ToJSOpt export async function yamlParseFile(path: string, options: YamlParseOptions = {}) { try { - return yamlParse(await readFile(path, "utf-8"), { + return yamlParse(await readTextFile(path), { ...options, customTags: [...WINDMILL_CUSTOM_TAGS, ...((options.customTags as ScalarTag[] | undefined) ?? [])], }); diff --git a/cli/test/utils_unit.test.ts b/cli/test/utils_unit.test.ts index e5bcea4b92..de95848524 100644 --- a/cli/test/utils_unit.test.ts +++ b/cli/test/utils_unit.test.ts @@ -4,7 +4,10 @@ */ import { expect, test, describe } from "bun:test"; -import { deepEqual, isFileResource, isFilesetResource, toCamel, capitalize, validateRequiredArgs } from "../src/utils/utils.ts"; +import { deepEqual, isFileResource, isFilesetResource, toCamel, capitalize, validateRequiredArgs, stripBom, readTextFile, readTextFileSync } from "../src/utils/utils.ts"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { getTypeStrFromPath, removeType, @@ -634,6 +637,71 @@ describe("validateRequiredArgs", () => { }); }); +// ============================================================================= +// BOM handling +// ============================================================================= + +describe("stripBom", () => { + test("strips UTF-8 BOM", () => { + expect(stripBom("hello")).toBe("hello"); + }); + + test("returns input unchanged when no BOM", () => { + expect(stripBom("hello")).toBe("hello"); + expect(stripBom("")).toBe(""); + }); +}); + +describe("readTextFile / readTextFileSync", () => { + const tmp = mkdtempSync(join(tmpdir(), "wmill-bom-")); + + test("reads plain UTF-8 file", async () => { + const f = join(tmp, "plain.txt"); + writeFileSync(f, Buffer.from("hello world", "utf-8")); + expect(await readTextFile(f)).toBe("hello world"); + expect(readTextFileSync(f)).toBe("hello world"); + }); + + test("strips UTF-8 BOM", async () => { + const f = join(tmp, "bom.txt"); + writeFileSync(f, Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from("hello", "utf-8")])); + expect(await readTextFile(f)).toBe("hello"); + expect(readTextFileSync(f)).toBe("hello"); + }); + + test("throws on UTF-16 LE BOM", async () => { + const f = join(tmp, "utf16le.txt"); + writeFileSync(f, Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from("hello", "utf16le")])); + await expect(readTextFile(f)).rejects.toThrow(/UTF-16 LE/); + expect(() => readTextFileSync(f)).toThrow(/UTF-16 LE/); + }); + + test("throws on UTF-16 BE BOM", async () => { + const f = join(tmp, "utf16be.txt"); + writeFileSync(f, Buffer.from([0xfe, 0xff, 0x00, 0x68])); + await expect(readTextFile(f)).rejects.toThrow(/UTF-16 BE/); + expect(() => readTextFileSync(f)).toThrow(/UTF-16 BE/); + }); + + test("throws on UTF-32 LE BOM", async () => { + const f = join(tmp, "utf32le.txt"); + writeFileSync(f, Buffer.from([0xff, 0xfe, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00])); + await expect(readTextFile(f)).rejects.toThrow(/UTF-32 LE/); + }); + + test("empty file reads as empty string", async () => { + const f = join(tmp, "empty.txt"); + writeFileSync(f, Buffer.alloc(0)); + expect(await readTextFile(f)).toBe(""); + expect(readTextFileSync(f)).toBe(""); + }); + + // cleanup + test("cleanup", () => { + rmSync(tmp, { recursive: true, force: true }); + }); +}); + // ============================================================================= // TarAsZip adapter // ============================================================================= From e98bdfd5c10d1897f5dad142bd26d7a24bc85294 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:58:26 +0200 Subject: [PATCH 06/28] add plugin skill refresh flag (#8913) Co-authored-by: Claude Opus 4.5 --- system_prompts/README.md | 12 ++++ system_prompts/generate.py | 115 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/system_prompts/README.md b/system_prompts/README.md index 05736dc55c..d3cd0b1e41 100644 --- a/system_prompts/README.md +++ b/system_prompts/README.md @@ -25,6 +25,17 @@ When SDK methods or the OpenFlow schema change, run: python system_prompts/generate.py ``` +To also refresh the standalone skills in a Claude plugin checkout: + +```bash +python system_prompts/generate.py --plugin-dir ~/windmill-claude-plugin +``` + +`--plugin-dir` accepts: +- the `windmill-claude-plugin` repo root +- a plugin root such as `plugins/windmill-code-plugin` +- a direct `skills/` directory + This will: 1. Parse TypeScript and Python SDK files to extract function signatures @@ -32,6 +43,7 @@ This will: 3. Parse the CLI commands 4. Assemble complete prompts from markdown files 5. Generate TypeScript exports in `auto-generated/` +6. Optionally refresh plugin-ready standalone `SKILL.md` files in the target directory ### Scope diff --git a/system_prompts/generate.py b/system_prompts/generate.py index b3e7588b97..b24fd6ca0b 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -11,11 +11,14 @@ This script: Usage: python generate.py + python generate.py --plugin-dir /path/to/windmill-claude-plugin """ +import argparse import ast import json import re +import shutil from pathlib import Path import yaml @@ -1104,13 +1107,121 @@ def generate_skills_ts_export(skills: list[str], schema_yaml_content: dict[str, return ts +def format_schema_for_markdown(schema_yaml: str, schema_name: str, file_pattern: str) -> str: + """Format a standalone schema block for plugin skill files.""" + return f"""## {schema_name} (`{file_pattern}`) + +Must be a YAML file that adheres to the following schema: + +```yaml +{schema_yaml.strip()} +```""" + + +def render_plugin_skill_content(skill_name: str, schema_yaml_content: dict[str, str]) -> str: + """Render plugin-ready skill content from generated base skill files.""" + skill_path = OUTPUT_SKILLS_DIR / skill_name / "SKILL.md" + if not skill_path.exists(): + raise FileNotFoundError(f"Missing generated skill content for {skill_name}: {skill_path}") + + skill_content = skill_path.read_text() + schema_mappings = SCHEMA_MAPPINGS.get(skill_name, []) + if not schema_mappings: + return skill_content + + schema_docs = [] + for schema_name, schema_key in schema_mappings: + schema_yaml = schema_yaml_content.get(schema_key) + if not schema_yaml: + continue + schema_docs.append( + format_schema_for_markdown( + schema_yaml=schema_yaml, + schema_name=schema_name, + file_pattern=f"*.{schema_key}.yaml", + ) + ) + + if not schema_docs: + return skill_content + + return f"{skill_content}\n\n" + "\n\n".join(schema_docs) + + +def resolve_plugin_skills_dir(plugin_dir: Path) -> Path: + """Resolve the plugin skills directory from a repo root, plugin root, or skills dir.""" + plugin_dir = plugin_dir.expanduser().resolve() + + repo_skills_dir = plugin_dir / "plugins" / "windmill-code-plugin" / "skills" + repo_plugin_json = plugin_dir / "plugins" / "windmill-code-plugin" / ".claude-plugin" / "plugin.json" + if repo_plugin_json.exists(): + return repo_skills_dir + + plugin_skills_dir = plugin_dir / "skills" + plugin_json = plugin_dir / ".claude-plugin" / "plugin.json" + if plugin_json.exists(): + return plugin_skills_dir + + if plugin_dir.name == "skills": + return plugin_dir + + return plugin_skills_dir + + +def generate_plugin_skills( + plugin_dir: Path, + skills: list[str], + schema_yaml_content: dict[str, str], +) -> Path: + """Generate standalone skills in a Claude plugin checkout.""" + skills_dir = resolve_plugin_skills_dir(plugin_dir) + skills_dir.mkdir(parents=True, exist_ok=True) + + expected_skills = set(skills) + for existing in skills_dir.iterdir(): + if existing.is_dir() and existing.name not in expected_skills: + shutil.rmtree(existing) + + for skill_name in skills: + skill_dir = skills_dir / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text( + render_plugin_skill_content(skill_name, schema_yaml_content) + ) + + print(f"\nGenerated for plugin:") + print(f" - {skills_dir} ({len(skills)} skills)") + return skills_dir + + # ============================================================================= # Main Entry Point # ============================================================================= +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description=( + "Generate Windmill system prompts, CLI guidance, and optionally " + "plugin-ready standalone skills." + ) + ) + parser.add_argument( + "--plugin-dir", + type=Path, + help=( + "Optional plugin target. Accepts a windmill-claude-plugin repo root, " + "a plugin root, or a skills directory, and refreshes standalone skills there." + ), + ) + return parser.parse_args() + + def main(): """Main generation function.""" + args = parse_args() + print("Generating system prompts documentation...") # Ensure output directories exist @@ -1344,6 +1455,10 @@ export function getDatatableSdkReference(): string { print(f" - auto-generated/schemas/ ({len(schema_yaml_content)} schema files)") print(f"\nGenerated for CLI:") print(f" - cli/src/guidance/skills.ts") + + if args.plugin_dir: + generate_plugin_skills(args.plugin_dir, skills, schema_yaml_content) + print("\nDone!") From 4e6e7a0407a0a7ee2ab745841538e3359e44bfa6 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 09:36:25 -0700 Subject: [PATCH 07/28] refactor(cli): extract fileset parent push helper (#8914) Co-authored-by: Claude Opus 4.7 (1M context) --- cli/src/commands/sync/sync.ts | 158 +++++++++++++++------------------- 1 file changed, 68 insertions(+), 90 deletions(-) diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index b08afb86e2..26ee585b3c 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -609,6 +609,48 @@ async function findFilesetResourceFile(changePath: string): Promise { throw new Error(`No resource metadata file found for fileset resource: ${changePath}`); } +type FilesetPushResult = + | { status: "pushed"; resourceFilePath: string } + | { status: "already-synced"; resourceFilePath: string } + | { status: "parent-missing" }; + +async function pushFilesetParentResource( + childPath: string, + workspaceId: string, + alreadySynced: string[], + cachedWsName: string | null, +): Promise { + let resourceFilePath: string; + try { + resourceFilePath = await findFilesetResourceFile(childPath); + } catch { + return { status: "parent-missing" }; + } + if (alreadySynced.includes(resourceFilePath)) { + return { status: "already-synced", resourceFilePath }; + } + alreadySynced.push(resourceFilePath); + + const newObj = parseFromPath( + resourceFilePath, + await readTextFile(resourceFilePath), + ); + + let serverPath = resourceFilePath; + if (cachedWsName && isWorkspaceSpecificFile(resourceFilePath)) { + serverPath = fromWorkspaceSpecificPath(resourceFilePath, cachedWsName); + } + + await pushResource( + workspaceId, + serverPath, + undefined, + newObj, + resourceFilePath, + ); + return { status: "pushed", resourceFilePath }; +} + function ZipFSElement( zip: JSZip, useYaml: boolean, @@ -3337,37 +3379,24 @@ export async function push( } } if (isFilesetResource(change.path)) { - const resourceFilePath = await findFilesetResourceFile(change.path); - if (!alreadySynced.includes(resourceFilePath)) { - alreadySynced.push(resourceFilePath); - - const newObj = parseFromPath( - resourceFilePath, - await readTextFile(resourceFilePath), - ); - - let serverPath = resourceFilePath; - const currentBranch = cachedWsNameForPush; - - if (currentBranch && isWorkspaceSpecificFile(resourceFilePath)) { - serverPath = fromWorkspaceSpecificPath( - resourceFilePath, - currentBranch, - ); - } - - await pushResource( - workspace.workspaceId, - serverPath, - undefined, - newObj, - resourceFilePath, + const result = await pushFilesetParentResource( + change.path, + workspace.workspaceId, + alreadySynced, + cachedWsNameForPush, + ); + if (result.status === "parent-missing") { + throw new Error( + `No resource metadata file found for fileset resource: ${change.path}`, ); + } + if (result.status === "pushed") { if (stateTarget) { await writeFile(stateTarget, change.after, "utf-8"); } continue; } + // "already-synced": fall through (pre-existing behavior). } const oldObj = parseFromPath(change.path, change.before); const newObj = parseFromPath(change.path, change.after); @@ -3399,39 +3428,14 @@ export async function push( } } else if (change.name === "added") { if (isFilesetResource(change.path)) { - // Re-push the parent resource so the new fileset file is included. - // If the parent is also being added here, its own push covers all children. - let resourceFilePath: string | undefined; - try { - resourceFilePath = await findFilesetResourceFile(change.path); - } catch { - continue; - } - if (alreadySynced.includes(resourceFilePath)) { - continue; - } - alreadySynced.push(resourceFilePath); - - const newObj = parseFromPath( - resourceFilePath, - await readFile(resourceFilePath, "utf-8"), - ); - - let serverPath = resourceFilePath; - const currentBranch = cachedWsNameForPush; - if (currentBranch && isWorkspaceSpecificFile(resourceFilePath)) { - serverPath = fromWorkspaceSpecificPath( - resourceFilePath, - currentBranch, - ); - } - - await pushResource( + // Re-push the parent resource (guarded by alreadySynced). + // Parent-missing means the parent itself is also being added and + // its own change will push the full fileset — safe to skip. + await pushFilesetParentResource( + change.path, workspace.workspaceId, - serverPath, - undefined, - newObj, - resourceFilePath, + alreadySynced, + cachedWsNameForPush, ); continue; } @@ -3522,40 +3526,14 @@ export async function push( continue; } if (isFilesetResource(change.path)) { - // Re-push the parent resource with the updated fileset contents. - // If the parent is also being deleted, its own "deleted" change - // removes the whole resource, so skip this child. - let resourceFilePath: string | undefined; - try { - resourceFilePath = await findFilesetResourceFile(change.path); - } catch { - continue; - } - if (alreadySynced.includes(resourceFilePath)) { - continue; - } - alreadySynced.push(resourceFilePath); - - const newObj = parseFromPath( - resourceFilePath, - await readFile(resourceFilePath, "utf-8"), - ); - - let serverPath = resourceFilePath; - const currentBranch = cachedWsNameForPush; - if (currentBranch && isWorkspaceSpecificFile(resourceFilePath)) { - serverPath = fromWorkspaceSpecificPath( - resourceFilePath, - currentBranch, - ); - } - - await pushResource( + // Re-push the parent resource (guarded by alreadySynced). + // Parent-missing means the parent itself is also being deleted + // and its own "deleted" change removes the whole resource. + await pushFilesetParentResource( + change.path, workspace.workspaceId, - serverPath, - undefined, - newObj, - resourceFilePath, + alreadySynced, + cachedWsNameForPush, ); continue; } From 131dd0682f4ef16f9c00c37d928b1d468f45a015 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Wed, 22 Apr 2026 18:37:16 +0200 Subject: [PATCH 08/28] restore bottom padding on script run page (#8916) Co-authored-by: Claude Opus 4.7 (1M context) --- frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index e2cd1097c9..f8ccbe9c7e 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -712,7 +712,7 @@ {/if} {/snippet} -
+
From eeb5d12be3ba2aedf2ebc4843d4395e241ecc8d3 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:27:10 +0200 Subject: [PATCH 09/28] fix: support windmill chat answer override (#8909) * fix: support windmill chat answer override Co-Authored-By: Claude Opus 4.5 * fix: remove output fallback from chat override Co-Authored-By: Claude Opus 4.5 * fix: handle non-string chat answer overrides Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- backend/windmill-worker/src/worker_flow.rs | 144 +++++++++++++++++---- 1 file changed, 122 insertions(+), 22 deletions(-) diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 6b759a575f..1abe2a75d9 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -1990,6 +1990,45 @@ fn find_flow_job_index(flow_jobs: &Vec, job_id_for_status: &Uuid) -> Optio flow_jobs.iter().position(|x| x == job_id_for_status) } +fn format_chat_message_value(value: &Value) -> String { + match value { + Value::Null => "null".to_string(), + Value::Bool(_) | Value::Number(_) => value.to_string(), + Value::String(text) => text.clone(), + Value::Array(_) | Value::Object(_) => serde_json::to_string_pretty(value) + .unwrap_or_else(|e| format!("Failed to serialize result: {e}")), + } +} + +/// Selects the assistant message persisted for the final non-AI step in a chat-enabled flow. +/// `windmill_chat_answer` is the explicit override contract: +/// - `null` suppresses the assistant message +/// - any other JSON value is rendered as the chat message +fn extract_chat_message_from_flow_result(result: &RawValue) -> error::Result> { + let value: Value = serde_json::from_str(result.get()) + .map_err(|e| Error::internal_err(format!("Failed to parse flow result: {e}")))?; + + match value { + Value::Object(map) => { + match map.get("windmill_chat_answer") { + Some(Value::Null) => return Ok(None), + Some(answer) => return Ok(Some(format_chat_message_value(answer))), + _ => {} + } + + Ok(Some( + serde_json::to_string_pretty(&Value::Object(map)) + .unwrap_or_else(|e| format!("Failed to serialize result: {e}")), + )) + } + Value::String(content) => Ok(Some(content)), + value => Ok(Some( + serde_json::to_string_pretty(&value) + .unwrap_or_else(|e| format!("Failed to serialize result: {e}")), + )), + } +} + async fn add_tool_message_to_conversation( db: &DB, job_id: &Uuid, @@ -2007,28 +2046,8 @@ async fn add_tool_message_to_conversation( if let Some(conversation_id) = conversation_id { // Only create assistant message if last module is NOT an AI agent, or there was an error if !is_ai_agent_step || success == false { - let value = serde_json::to_value(result.get()) - .map_err(|e| Error::internal_err(format!("Failed to serialize result: {e}")))?; - - let content = match value { - // If it's an Object with "output" key AND the output is a String, return it - serde_json::Value::Object(mut map) - if map.contains_key("output") - && matches!(map.get("output"), Some(serde_json::Value::String(_))) => - { - if let Some(serde_json::Value::String(s)) = map.remove("output") { - s - } else { - // prettify the whole result - serde_json::to_string_pretty(&map) - .unwrap_or_else(|e| format!("Failed to serialize result: {e}")) - } - } - // Otherwise, if the whole value is a String, return it - serde_json::Value::String(s) => s, - // Otherwise, prettify the whole result - v => serde_json::to_string_pretty(&v) - .unwrap_or_else(|e| format!("Failed to serialize result: {e}")), + let Some(content) = extract_chat_message_from_flow_result(result.as_ref())? else { + return Ok(()); }; // Insert new assistant message @@ -5614,3 +5633,84 @@ pub async fn get_previous_job_result( _ => Ok(None), } } + +#[cfg(test)] +mod tests { + use super::extract_chat_message_from_flow_result; + use serde_json::{json, value::to_raw_value}; + + #[test] + fn pretty_prints_full_result_when_no_override_is_present() { + let value = json!({ + "output": "final answer", + "metadata": { "foo": "bar" } + }); + let result = to_raw_value(&value).unwrap(); + + let message = extract_chat_message_from_flow_result(result.as_ref()).unwrap(); + + assert_eq!(message, Some(serde_json::to_string_pretty(&value).unwrap())); + } + + #[test] + fn uses_windmill_chat_answer_when_it_is_a_string() { + let result = to_raw_value(&json!({ + "windmill_chat_answer": "chat-visible answer", + "output": "ignored output", + "metadata": { "foo": "bar" } + })) + .unwrap(); + + let message = extract_chat_message_from_flow_result(result.as_ref()).unwrap(); + + assert_eq!(message, Some("chat-visible answer".to_string())); + } + + #[test] + fn skips_persisting_when_windmill_chat_answer_is_null() { + let result = to_raw_value(&json!({ + "windmill_chat_answer": null, + "output": "should not be stored" + })) + .unwrap(); + + let message = extract_chat_message_from_flow_result(result.as_ref()).unwrap(); + + assert_eq!(message, None); + } + + #[test] + fn coerces_scalar_windmill_chat_answer_to_a_string() { + let result = to_raw_value(&json!({ + "windmill_chat_answer": 42, + "output": "ignored output", + "metadata": { "foo": "bar" } + })) + .unwrap(); + + let message = extract_chat_message_from_flow_result(result.as_ref()).unwrap(); + + assert_eq!(message, Some("42".to_string())); + } + + #[test] + fn pretty_prints_structured_windmill_chat_answer_only() { + let override_value = json!({ + "text": "chat-visible answer", + "meta": ["a", "b"] + }); + let result = to_raw_value(&json!({ + "windmill_chat_answer": override_value, + "output": "ignored output", + "metadata": { "foo": "bar" } + })) + .unwrap(); + + let message = extract_chat_message_from_flow_result(result.as_ref()).unwrap(); + + assert_eq!( + message, + Some(serde_json::to_string_pretty(&override_value).unwrap()) + ); + } +} From 18eed92dd66d635305a72818e2fcf0ee8b9672cc Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 19:31:34 +0000 Subject: [PATCH 10/28] fix: add aws-config to private feature to restore ce build Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/windmill-common/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/windmill-common/Cargo.toml b/backend/windmill-common/Cargo.toml index f225ec0c20..88a7706b88 100644 --- a/backend/windmill-common/Cargo.toml +++ b/backend/windmill-common/Cargo.toml @@ -9,7 +9,7 @@ default = [] enterprise = ["dep:aws-config"] instance_config_schema = ["dep:schemars"] local_reports = ["dep:rsa", "dep:aes-gcm"] -private = ["dep:aws-sdk-rds", "dep:aws-sdk-secretsmanager"] +private = ["dep:aws-sdk-rds", "dep:aws-sdk-secretsmanager", "dep:aws-config"] jemalloc = ["dep:tikv-jemalloc-ctl"] tantivy = [] prometheus = ["dep:prometheus"] From bbb564c1420593014d17f352ba38d3ed38c248e1 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 19:31:37 +0000 Subject: [PATCH 11/28] fix: omit default_permissioned_as from tarball export when empty Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/windmill-api/src/workspaces_export.rs | 7 +++++++ cli/test/folder_default_permissioned_as.test.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 98d46b1421..2e59081328 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -237,6 +237,13 @@ where if !preserve_extra_perms && obj.contains_key("extra_perms") { obj.remove("extra_perms"); } + if obj + .get("default_permissioned_as") + .and_then(|v| v.as_array()) + .is_some_and(|a| a.is_empty()) + { + obj.remove("default_permissioned_as"); + } serde_json::to_string_pretty(&obj).ok() }) diff --git a/cli/test/folder_default_permissioned_as.test.ts b/cli/test/folder_default_permissioned_as.test.ts index 5af16d3355..7116fb9fa2 100644 --- a/cli/test/folder_default_permissioned_as.test.ts +++ b/cli/test/folder_default_permissioned_as.test.ts @@ -216,8 +216,8 @@ describe("folder default_permissioned_as", () => { // Step 3: Locally edit folder.meta.yaml to add default_permissioned_as rules const metaPath = join(tempDir, "f", folderName, "folder.meta.yaml"); const metaContent = await readFile(metaPath, "utf-8"); - // Backend always emits the field; initially it's an empty array - expect(metaContent).toContain("default_permissioned_as: []"); + // Backend omits the field when the rule list is empty + expect(metaContent).not.toContain("default_permissioned_as:"); const newMeta = `display_name: ${folderName} owners: From e73770c2478634a86c6360bbcd4c8b5444150eb5 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 13:05:08 -0700 Subject: [PATCH 12/28] fix: derive debug signing key deterministically from JWT_SECRET (#8917) Previously each API replica generated a random Ed25519 signing key at startup (unless DEBUG_SIGNING_KEY_SEED was set). In multi-replica deployments this caused "Invalid JWT signature" rejections in the multiplayer server: the browser could sign a token on pod A while `windmill-extra` had cached the JWKS public key from pod B. Derive the seed deterministically from the DB-backed JWT_SECRET using SHA-256 with a domain-separation tag so all pods agree without coordination. Re-derive on JWT_SECRET rotation. The DEBUG_SIGNING_KEY_SEED env var is still honored as an override. Co-authored-by: Claude Opus 4.7 (1M context) --- backend/Cargo.lock | 1 - backend/src/monitor.rs | 4 ++ backend/windmill-api-debug/Cargo.toml | 1 - backend/windmill-api-debug/src/lib.rs | 70 +++++++++++++++++++-------- backend/windmill-api/src/lib.rs | 2 + 5 files changed, 57 insertions(+), 21 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index da8ae12243..b703a76711 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -16242,7 +16242,6 @@ dependencies = [ "ed25519-dalek", "hex", "lazy_static", - "rand 0.9.0", "serde", "serde_json", "sha2 0.10.9", diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 175965e4ad..c360bb7b8d 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -3879,6 +3879,10 @@ pub async fn reload_jwt_secret_setting(db: &DB) -> error::Result<()> { JWT_SECRET.store(std::sync::Arc::new(jwt_secret)); + // The debug signing key is derived from JWT_SECRET, so re-derive it here so + // rotation propagates to /api/debug/* signing without requiring a restart. + windmill_api::reload_debug_signing_key().await; + Ok(()) } diff --git a/backend/windmill-api-debug/Cargo.toml b/backend/windmill-api-debug/Cargo.toml index de94a3aba3..8531407416 100644 --- a/backend/windmill-api-debug/Cargo.toml +++ b/backend/windmill-api-debug/Cargo.toml @@ -18,7 +18,6 @@ chrono.workspace = true ed25519-dalek.workspace = true hex.workspace = true lazy_static.workspace = true -rand.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true diff --git a/backend/windmill-api-debug/src/lib.rs b/backend/windmill-api-debug/src/lib.rs index baece9e8e9..61ec59be75 100644 --- a/backend/windmill-api-debug/src/lib.rs +++ b/backend/windmill-api-debug/src/lib.rs @@ -37,7 +37,7 @@ use tokio::sync::RwLock; use uuid::Uuid; use windmill_audit::{audit_oss::audit_log, ActionKind}; use windmill_common::{ - db::UserDB, error::JsonResult, jobs::JobKind, scripts::ScriptLang, + db::UserDB, error::JsonResult, jobs::JobKind, jwt::JWT_SECRET, scripts::ScriptLang, users::username_to_permissioned_as, }; @@ -48,35 +48,67 @@ pub const DEBUG_TOKEN_TTL_SECS: i64 = 60; lazy_static::lazy_static! { /// Ed25519 signing key for debug tokens. - /// Generated at startup if not provided via environment variable. + /// + /// Derived deterministically from the instance `JWT_SECRET` so all API + /// replicas agree on the same key without coordination. Refreshed via + /// [`reload_debug_signing_key`] when `JWT_SECRET` is reloaded. static ref DEBUG_SIGNING_KEY: Arc>> = Arc::new(RwLock::new(None)); } -/// Initialize the debug signing key. -/// Call this at server startup. -pub async fn init_debug_signing_key() { - let mut key_guard = DEBUG_SIGNING_KEY.write().await; +/// Domain-separation tag so the debug Ed25519 seed cannot be confused with +/// any other HMAC/HS256 usage of `JWT_SECRET`. +const DEBUG_KEY_DERIVATION_TAG: &[u8] = b"windmill-debug-signing-key:v1:"; - // Check if key is provided via environment variable (base64-encoded seed) +fn derive_signing_key_from_jwt_secret(jwt_secret: &str) -> SigningKey { + let mut hasher = Sha256::new(); + hasher.update(DEBUG_KEY_DERIVATION_TAG); + hasher.update(jwt_secret.as_bytes()); + let seed: [u8; 32] = hasher.finalize().into(); + SigningKey::from_bytes(&seed) +} + +fn compute_debug_signing_key() -> Option { + // Env var override: base64url-encoded 32-byte seed. Useful for tests or + // advanced deployments that want to pin the key independently. if let Ok(seed_b64) = std::env::var("DEBUG_SIGNING_KEY_SEED") { - if let Ok(seed_bytes) = URL_SAFE_NO_PAD.decode(&seed_b64) { - if seed_bytes.len() >= 32 { + match URL_SAFE_NO_PAD.decode(&seed_b64) { + Ok(seed_bytes) if seed_bytes.len() >= 32 => { let mut seed = [0u8; 32]; seed.copy_from_slice(&seed_bytes[..32]); - *key_guard = Some(SigningKey::from_bytes(&seed)); - tracing::info!("Debug signing key loaded from environment"); - return; + tracing::info!("Debug signing key loaded from DEBUG_SIGNING_KEY_SEED"); + return Some(SigningKey::from_bytes(&seed)); } + _ => tracing::warn!( + "Invalid DEBUG_SIGNING_KEY_SEED (expect base64url-encoded 32+ bytes); falling back to JWT_SECRET derivation" + ), } - tracing::warn!("Invalid DEBUG_SIGNING_KEY_SEED, generating new key"); } - // Generate a new random key using rand - let mut seed = [0u8; 32]; - rand::Rng::fill(&mut rand::rng(), &mut seed); - let signing_key = SigningKey::from_bytes(&seed); - tracing::info!("Generated new debug signing key"); - *key_guard = Some(signing_key); + let jwt_secret = JWT_SECRET.load(); + if jwt_secret.is_empty() { + return None; + } + Some(derive_signing_key_from_jwt_secret(&jwt_secret)) +} + +/// Initialize the debug signing key. Call once at server startup, after +/// `reload_jwt_secret_setting` so `JWT_SECRET` is populated. +pub async fn init_debug_signing_key() { + reload_debug_signing_key().await; +} + +/// Recompute and store the debug signing key. Call after `JWT_SECRET` is +/// (re)loaded so rotation propagates without a pod restart. +pub async fn reload_debug_signing_key() { + let key = compute_debug_signing_key(); + if key.is_none() { + tracing::warn!( + "Debug signing key not initialized: JWT_SECRET is empty and DEBUG_SIGNING_KEY_SEED is not set. /api/debug/* endpoints will return an error." + ); + } else { + tracing::info!("Debug signing key initialized from JWT_SECRET"); + } + *DEBUG_SIGNING_KEY.write().await = key; } pub fn global_service() -> Router { diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index c11f89a1c4..042aa98a14 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -216,6 +216,8 @@ pub use windmill_common::utils::HTTP_CLIENT_PERMISSIVE as HTTP_CLIENT; pub use windmill_common::utils::{COOKIE_DOMAIN, IS_SECURE}; +pub use windmill_api_debug::reload_debug_signing_key; + #[cfg(feature = "oauth2")] pub use windmill_oauth::OAUTH_CLIENTS; From 6abe33109a0197522ea6729187472cb2d827dd86 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 22 Apr 2026 13:06:31 -0700 Subject: [PATCH 13/28] chore(main): release 1.689.0 (#8894) * chore(main): release 1.689.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> --- CHANGELOG.md | 27 + backend/Cargo.lock | 934 ++++++++++-------- backend/Cargo.toml | 4 +- .../parsers/windmill-parser-wasm/Cargo.lock | 48 +- .../parsers/windmill-parser-wasm/Cargo.toml | 2 +- backend/windmill-api/openapi.yaml | 2 +- benchmarks/lib.ts | 2 +- cli/src/main.ts | 2 +- frontend/package-lock.json | 54 +- frontend/package.json | 2 +- lsp/Pipfile | 2 +- openflow.openapi.yaml | 2 +- .../WindmillClient/WindmillClient.psd1 | 2 +- python-client/wmill/pyproject.toml | 2 +- typescript-client/jsr.json | 2 +- typescript-client/package.json | 2 +- version.txt | 2 +- 17 files changed, 652 insertions(+), 439 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6758b6eb..1e112ecb7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [1.689.0](https://github.com/windmill-labs/windmill/compare/v1.688.0...v1.689.0) (2026-04-22) + + +### Features + +* add s3 stream progress logs to other DB executors ([#8898](https://github.com/windmill-labs/windmill/issues/8898)) ([2d4fadb](https://github.com/windmill-labs/windmill/commit/2d4fadb590590837412d638192fbd62bdc9331e8)) +* allow hiding catalog picker and raw input on s3 form fields ([#8902](https://github.com/windmill-labs/windmill/issues/8902)) ([05baa4a](https://github.com/windmill-labs/windmill/commit/05baa4ab026a307267d11fb827f8abcc246d1ac0)) +* async dep endpoints and queue-position logs in cli ([#8895](https://github.com/windmill-labs/windmill/issues/8895)) ([aaf3a19](https://github.com/windmill-labs/windmill/commit/aaf3a1974746be451adf463fd1f0e584b2fa995e)) +* auto-strip UTF-8 BOM when reading local files in CLI ([#8911](https://github.com/windmill-labs/windmill/issues/8911)) ([99bc96d](https://github.com/windmill-labs/windmill/commit/99bc96d0b231a2af303b28aa87d5de9141ee5cab)) + + +### Bug Fixes + +* add aws-config to private feature to restore ce build ([18eed92](https://github.com/windmill-labs/windmill/commit/18eed92dd66d635305a72818e2fcf0ee8b9672cc)) +* add flow conversation token scope ([#8903](https://github.com/windmill-labs/windmill/issues/8903)) ([aea7444](https://github.com/windmill-labs/windmill/commit/aea74445a31d30abb8030763db91e1829db772f0)) +* add proxy eval coverage for gemini schemas ([#8897](https://github.com/windmill-labs/windmill/issues/8897)) ([fddd8e2](https://github.com/windmill-labs/windmill/commit/fddd8e288fc0fd7af3b1df1ddd4476fb57694ed3)) +* apply powershell workspace dependencies to deployed scripts ([#8912](https://github.com/windmill-labs/windmill/issues/8912)) ([dc89673](https://github.com/windmill-labs/windmill/commit/dc896737ac1dcd90ab96314b2bc2f044ff833b8a)) +* detect and clearly label OOM in zombie flow alerts ([#8901](https://github.com/windmill-labs/windmill/issues/8901)) ([680c711](https://github.com/windmill-labs/windmill/commit/680c711f9262683c046a78532b22be5f5a4121a8)) +* omit default_permissioned_as from tarball export when empty ([bbb564c](https://github.com/windmill-labs/windmill/commit/bbb564c1420593014d17f352ba38d3ed38c248e1)) +* persist flow groups from AI chat tool calls ([#8906](https://github.com/windmill-labs/windmill/issues/8906)) ([932d183](https://github.com/windmill-labs/windmill/commit/932d18331196ef3e87c45d8a06ae45ac8013bd7a)) +* push parent resource on fileset child add/delete ([#8910](https://github.com/windmill-labs/windmill/issues/8910)) ([f29badc](https://github.com/windmill-labs/windmill/commit/f29badcf368e7c712f1515fa30a6a0e179a4bdc5)) +* rust nsjail RUSTUP_HOME mount and arch-aware cache keys ([#8890](https://github.com/windmill-labs/windmill/issues/8890)) ([f8c916c](https://github.com/windmill-labs/windmill/commit/f8c916cb6073f5566289c395ec39ec3919f449e7)) +* skip opus 4.7 sampling params ([#8904](https://github.com/windmill-labs/windmill/issues/8904)) ([1e83278](https://github.com/windmill-labs/windmill/commit/1e83278fe2ef5a5c6959a9351e7286d9dbf2453a)) +* support windmill chat answer override ([#8909](https://github.com/windmill-labs/windmill/issues/8909)) ([eeb5d12](https://github.com/windmill-labs/windmill/commit/eeb5d12be3ba2aedf2ebc4843d4395e241ecc8d3)) +* track dollar-quoted strings in SQL block splitter ([#8891](https://github.com/windmill-labs/windmill/issues/8891)) ([53badf1](https://github.com/windmill-labs/windmill/commit/53badf1a8cff576bb4ccbc75f045efb457b6a07d)) +* trigger failure_module when branchone predicate throws ([#8905](https://github.com/windmill-labs/windmill/issues/8905)) ([dffb89e](https://github.com/windmill-labs/windmill/commit/dffb89e00632bd4e7bbe9998bccb1f14357d9c07)), closes [#8889](https://github.com/windmill-labs/windmill/issues/8889) + ## [1.688.0](https://github.com/windmill-labs/windmill/compare/v1.687.0...v1.688.0) (2026-04-20) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index b703a76711..1bf072ee99 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -39,7 +39,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -65,7 +65,7 @@ checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if", "cipher 0.3.0", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", ] @@ -77,7 +77,7 @@ checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher 0.4.4", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -234,9 +234,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -249,7 +249,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -421,7 +421,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.12.0", + "indexmap 2.14.0", "lexical-core", "memchr", "num", @@ -602,7 +602,7 @@ dependencies = [ "futures-core", "libc", "portable-atomic", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "tokio", "tokio-stream", "xattr", @@ -860,9 +860,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -870,9 +870,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -1137,7 +1137,7 @@ dependencies = [ "bytes", "form_urlencoded", "hex", - "hmac", + "hmac 0.12.1", "http 0.2.12", "http 1.4.0", "percent-encoding", @@ -1226,9 +1226,9 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.9", "hyper-util", "pin-project-lite", "rustls 0.21.12", @@ -1420,7 +1420,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "itoa", "matchit 0.8.4", @@ -1645,7 +1645,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "shlex", "syn 2.0.117", ] @@ -1665,7 +1665,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "shlex", "syn 2.0.117", ] @@ -1747,16 +1747,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq 0.4.2", - "cpufeatures", + "cpufeatures 0.3.0", ] [[package]] @@ -1784,6 +1784,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-modes" version = "0.8.1" @@ -1823,7 +1832,7 @@ dependencies = [ "hex", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-named-pipe", "hyper-util", "hyperlocal", @@ -2123,7 +2132,7 @@ dependencies = [ "rayon", "safetensors", "thiserror 2.0.18", - "yoke 0.8.1", + "yoke 0.8.2", "zip", ] @@ -2204,9 +2213,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -2253,6 +2262,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -2302,7 +2322,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -2319,9 +2339,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -2341,9 +2361,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2368,13 +2388,19 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -2472,6 +2498,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -2500,9 +2532,9 @@ checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", "konst", @@ -2612,6 +2644,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -2747,6 +2788,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.3.1" @@ -2777,6 +2827,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2784,7 +2843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto 0.2.9", @@ -3144,7 +3203,7 @@ dependencies = [ "base64 0.22.1", "half", "hashbrown 0.14.5", - "indexmap 2.12.0", + "indexmap 2.14.0", "libc", "log", "object_store", @@ -3323,7 +3382,7 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr-common", - "indexmap 2.12.0", + "indexmap 2.14.0", "paste", "recursive", "serde_json", @@ -3338,7 +3397,7 @@ checksum = "422ac9cf3b22bbbae8cdf8ceb33039107fde1b5492693168f13bd566b1bcc839" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.12.0", + "indexmap 2.14.0", "itertools 0.14.0", "paste", ] @@ -3492,7 +3551,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-physical-expr", - "indexmap 2.12.0", + "indexmap 2.14.0", "itertools 0.14.0", "log", "recursive", @@ -3515,7 +3574,7 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.14.5", - "indexmap 2.12.0", + "indexmap 2.14.0", "itertools 0.14.0", "log", "paste", @@ -3577,7 +3636,7 @@ dependencies = [ "futures", "half", "hashbrown 0.14.5", - "indexmap 2.12.0", + "indexmap 2.14.0", "itertools 0.14.0", "log", "parking_lot", @@ -3619,7 +3678,7 @@ dependencies = [ "bigdecimal", "datafusion-common", "datafusion-expr", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "recursive", "regex", @@ -3737,7 +3796,7 @@ dependencies = [ "deno_media_type", "deno_path_util", "http 1.4.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "once_cell", "parking_lot", @@ -3778,7 +3837,7 @@ dependencies = [ "glob", "ignore", "import_map", - "indexmap 2.12.0", + "indexmap 2.14.0", "jsonc-parser", "log", "percent-encoding", @@ -3819,7 +3878,7 @@ dependencies = [ "deno_path_util", "deno_unsync", "futures", - "indexmap 2.12.0", + "indexmap 2.14.0", "libc", "memoffset", "parking_lot", @@ -3871,7 +3930,7 @@ dependencies = [ "aes-kw", "base64 0.21.7", "cbc", - "const-oid", + "const-oid 0.9.6", "ctr", "curve25519-dalek", "deno_core", @@ -3946,8 +4005,8 @@ dependencies = [ "hickory-resolver", "http 1.4.0", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "ipnet", "percent-encoding", @@ -4034,7 +4093,7 @@ dependencies = [ "http 1.4.0", "httparse", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "itertools 0.10.5", "memmem", @@ -4200,7 +4259,7 @@ dependencies = [ "brotli 6.0.0", "bytes", "cbc", - "const-oid", + "const-oid 0.9.6", "ctr", "data-encoding", "deno_core", @@ -4227,10 +4286,10 @@ dependencies = [ "hkdf", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "idna", - "indexmap 2.12.0", + "indexmap 2.14.0", "ipnetwork", "k256", "lazy-regex", @@ -4306,7 +4365,7 @@ version = "0.212.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d328067139909aa81522a5d90f119368b541fbddd73ab630e4d9f777865f0d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "proc-macro-rules", "proc-macro2", "quote", @@ -4350,7 +4409,7 @@ dependencies = [ "deno_error", "deno_path_util", "deno_semver", - "indexmap 2.12.0", + "indexmap 2.14.0", "serde", "serde_json", "sys_traits", @@ -4498,7 +4557,7 @@ dependencies = [ "http 1.4.0", "http-body-util", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "libc", "log", @@ -4552,8 +4611,8 @@ dependencies = [ "deno_error", "deno_tls", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "log", "once_cell", @@ -4682,7 +4741,7 @@ dependencies = [ "h2 0.4.13", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "once_cell", "rustls-tokio-stream", @@ -4787,7 +4846,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "der_derive", "pem-rfc7468", "zeroize", @@ -4973,11 +5032,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", - "crypto-common", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "dirs" version = "4.0.0" @@ -5573,9 +5644,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fastwebsockets" @@ -5586,7 +5657,7 @@ dependencies = [ "base64 0.21.7", "bytes", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project", "rand 0.8.5", @@ -6203,6 +6274,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -6458,9 +6530,9 @@ dependencies = [ [[package]] name = "gzip-header" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +checksum = "86848f4fd157d91041a62c78046fb7b248bcc2dce78376d436a1756e9a038577" dependencies = [ "crc32fast", ] @@ -6477,7 +6549,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -6496,7 +6568,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -6571,11 +6643,18 @@ dependencies = [ ] [[package]] -name = "hashify" -version = "0.2.7" +name = "hashbrown" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "149e3ea90eb5a26ad354cfe3cb7f7401b9329032d0235f2687d03a35f30e5d4c" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashify" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1246c0e5493286aeb2dde35b1f4eb9c4ce00e628641210a5e553fc001a1f26" dependencies = [ + "indexmap 2.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6756,7 +6835,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -6768,6 +6847,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + [[package]] name = "home" version = "0.5.12" @@ -6886,7 +6974,7 @@ dependencies = [ "futures", "http 1.4.0", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.26.0", "hyper-tls", "hyper-tungstenite", @@ -6910,6 +6998,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -6936,9 +7033,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -6951,7 +7048,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -6967,8 +7063,8 @@ dependencies = [ "futures-util", "headers", "http 1.4.0", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-tls", "hyper-util", "native-tls", @@ -6987,7 +7083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7019,7 +7115,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "log", "rustls 0.22.4", @@ -7032,21 +7128,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "log", "rustls 0.23.35", "rustls-native-certs 0.8.3", - "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -7055,7 +7150,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7070,7 +7165,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "native-tls", "tokio", @@ -7085,7 +7180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a343d17fe7885302ed7252767dc7bb83609a874b6ff581142241ec4b73957ad" dependencies = [ "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7105,7 +7200,7 @@ dependencies = [ "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", @@ -7126,7 +7221,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -7159,22 +7254,23 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.1", + "utf8_iter", + "yoke 0.8.2", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -7185,9 +7281,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -7199,15 +7295,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -7219,20 +7315,20 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerotrie", "zerovec", @@ -7314,7 +7410,7 @@ checksum = "1215d4d92511fbbdaea50e750e91f2429598ef817f02b579158e92803b52c00a" dependencies = [ "boxed_error", "deno_error", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "percent-encoding", "serde", @@ -7336,12 +7432,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -7397,18 +7493,18 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ "bitflags 2.9.4", "cfg-if", @@ -7445,9 +7541,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -7698,7 +7794,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -7707,7 +7803,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", ] [[package]] @@ -7729,9 +7825,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "konst" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f0e13e6483b8c34885f7e6c9f19b1a7bd449c673fbb948a51c99d66ef74f4" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" dependencies = [ "konst_macro_rules", ] @@ -7790,9 +7886,9 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-http-proxy", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.9", "hyper-timeout", "hyper-util", "jsonpath-rust", @@ -7981,9 +8077,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libffi" @@ -8065,14 +8161,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags 2.9.4", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.4", ] [[package]] @@ -8109,9 +8205,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -8139,9 +8235,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -8191,18 +8287,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.14.0" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.0", ] @@ -8448,6 +8535,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.2", +] + [[package]] name = "md4" version = "0.10.2" @@ -8620,9 +8717,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -8726,9 +8823,9 @@ dependencies = [ [[package]] name = "mysql_async" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277ce2f2459b2af4cc6d0a0b7892381f80800832f57c533f03e2845f4ea331ea" +checksum = "d1d9585dc9058886ff3a1f48a23024dd1d054264dee7c5ae0e4bd640c953bee5" dependencies = [ "bytes", "crossbeam-queue", @@ -8737,7 +8834,7 @@ dependencies = [ "futures-sink", "futures-util", "keyed_priority_queue", - "lru 0.14.0", + "lru 0.16.4", "mysql_common", "native-tls", "pem 3.0.6", @@ -8793,7 +8890,7 @@ dependencies = [ "bitflags 2.9.4", "codespan-reporting", "hexf-parse", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "num-traits", "rustc-hash 1.1.0", @@ -9084,7 +9181,7 @@ dependencies = [ "dirs-sys 0.4.1", "fancy-regex 0.14.0", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "lru 0.12.5", "miette", @@ -9349,7 +9446,7 @@ dependencies = [ "http-body-util", "httparse", "humantime", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "md-5 0.10.6", "parking_lot", @@ -9460,7 +9557,7 @@ dependencies = [ "chrono", "dyn-clone", "ed25519-dalek", - "hmac", + "hmac 0.12.1", "http 1.4.0", "itertools 0.10.5", "log", @@ -9483,9 +9580,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.9.4", "cfg-if", @@ -9521,18 +9618,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -9768,9 +9865,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0218004a4aae742209bee9c3cef05672f6b2708be36a50add8eb613b1f2a4008" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] @@ -9945,9 +10042,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" [[package]] name = "path-clean" @@ -9968,7 +10065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -10067,7 +10164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.0", + "indexmap 2.14.0", ] [[package]] @@ -10233,9 +10330,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -10263,7 +10360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -10306,7 +10403,7 @@ dependencies = [ "byteorder", "bytes", "fallible-iterator 0.2.0", - "hmac", + "hmac 0.12.1", "md-5 0.10.6", "memchr", "rand 0.8.5", @@ -10316,19 +10413,19 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" dependencies = [ "base64 0.22.1", "byteorder", "bytes", "fallible-iterator 0.2.0", - "hmac", - "md-5 0.10.6", + "hmac 0.13.0", + "md-5 0.11.0", "memchr", - "rand 0.9.0", - "sha2 0.10.9", + "rand 0.10.1", + "sha2 0.11.0", "stringprep", ] @@ -10353,7 +10450,7 @@ dependencies = [ "bytes", "chrono", "fallible-iterator 0.2.0", - "postgres-protocol 0.6.10", + "postgres-protocol 0.6.11", "serde", "serde_json", "uuid", @@ -10361,9 +10458,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -10502,7 +10599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1" dependencies = [ "futures", - "indexmap 2.12.0", + "indexmap 2.14.0", "nix 0.30.1", "tokio", "tracing", @@ -10608,9 +10705,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ "ar_archive_writer", "cc", @@ -10720,7 +10817,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls 0.23.35", "socket2 0.6.3", "thiserror 2.0.18", @@ -10741,7 +10838,7 @@ dependencies = [ "lru-slab", "rand 0.9.0", "ring 0.17.14", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls 0.23.35", "rustls-pki-types", "slab", @@ -10837,6 +10934,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -10894,6 +11002,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" version = "0.5.1" @@ -10936,9 +11050,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -11049,9 +11163,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags 2.9.4", ] @@ -11173,8 +11287,8 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-tls", "hyper-util", "js-sys", @@ -11203,7 +11317,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -11221,8 +11335,8 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "js-sys", "log", @@ -11277,7 +11391,7 @@ dependencies = [ "futures", "getrandom 0.2.17", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "reqwest 0.13.1", "reqwest-middleware", "retry-policies", @@ -11308,7 +11422,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -11470,7 +11584,7 @@ dependencies = [ "convert_case 0.10.0", "fnv", "ident_case", - "indexmap 2.12.0", + "indexmap 2.14.0", "proc-macro-crate", "proc-macro2", "quote", @@ -11493,7 +11607,7 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", + "const-oid 0.9.6", "digest 0.10.7", "num-bigint-dig", "num-integer", @@ -11588,9 +11702,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" dependencies = [ "arrayvec", "borsh", @@ -11601,6 +11715,7 @@ dependencies = [ "rkyv", "serde", "serde_json", + "wasm-bindgen", ] [[package]] @@ -11617,9 +11732,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -11636,7 +11751,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", + "semver 1.0.28", ] [[package]] @@ -11711,7 +11826,7 @@ dependencies = [ "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -11795,10 +11910,10 @@ dependencies = [ "rustls 0.23.35", "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.13", "security-framework 3.6.0", "security-framework-sys", - "webpki-root-certs 1.0.6", + "webpki-root-certs 1.0.7", "windows-sys 0.61.2", ] @@ -11843,9 +11958,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -12230,9 +12345,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "semver-parser" @@ -12336,7 +12451,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -12429,7 +12544,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde", @@ -12457,7 +12572,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -12470,7 +12585,7 @@ version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "itoa", "libyml", "memchr", @@ -12522,7 +12637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -12534,7 +12649,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -12546,15 +12661,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] -name = "sha3" -version = "0.10.8" +name = "sha2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -12647,9 +12773,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd-json" @@ -12828,7 +12954,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "unicode-id-start", @@ -12963,7 +13089,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink 0.10.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "memchr", "once_cell", @@ -13044,7 +13170,7 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", "md-5 0.10.6", @@ -13085,7 +13211,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", @@ -13134,9 +13260,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" +checksum = "2c5e6deb40826033bd7b11c7ef25ef71193fabd71f680f40dd16538a2704d2f4" dependencies = [ "bytes", "futures-util", @@ -13153,15 +13279,15 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -13363,7 +13489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4740e53eaf68b101203c1df0937d5161a29f3c13bceed0836ddfe245b72dd000" dependencies = [ "anyhow", - "indexmap 2.12.0", + "indexmap 2.14.0", "serde", "serde_json", "swc_cached", @@ -13475,7 +13601,7 @@ checksum = "65f21494e75d0bd8ef42010b47cabab9caaed8f2207570e809f6f4eb51a710d1" dependencies = [ "better_scoped_tls", "bitflags 2.9.4", - "indexmap 2.12.0", + "indexmap 2.14.0", "once_cell", "phf 0.11.3", "rustc-hash 1.1.0", @@ -13544,7 +13670,7 @@ checksum = "76c76d8b9792ce51401d38da0fa62158d61f6d80d16d68fe5b03ce4bf5fba383" dependencies = [ "base64 0.21.7", "dashmap 5.5.3", - "indexmap 2.12.0", + "indexmap 2.14.0", "once_cell", "serde", "sha1", @@ -13584,7 +13710,7 @@ version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029eec7dd485923a75b5a45befd04510288870250270292fc2c1b3a9e7547408" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "num_cpus", "once_cell", "rustc-hash 1.1.0", @@ -13657,6 +13783,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -13822,7 +13954,7 @@ dependencies = [ "itertools 0.14.0", "levenshtein_automata", "log", - "lru 0.16.3", + "lru 0.16.4", "lz4_flex 0.13.0", "measure_time", "memmap2 0.9.10", @@ -13831,7 +13963,7 @@ dependencies = [ "rayon", "regex", "rust-stemmers", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "sketches-ddsketch", @@ -13904,7 +14036,7 @@ source = "git+https://github.com/windmill-labs/tantivy?rev=6ae7c70bc603b8e69e27f dependencies = [ "fnv", "nom 7.1.3", - "ordered-float 5.2.0", + "ordered-float 5.3.0", "serde", "serde_json", ] @@ -14181,9 +14313,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -14262,7 +14394,7 @@ dependencies = [ "bytes", "io-uring", "libc", - "mio 1.1.1", + "mio 1.2.0", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -14371,7 +14503,7 @@ dependencies = [ "percent-encoding", "phf 0.11.3", "pin-project-lite", - "postgres-protocol 0.6.10", + "postgres-protocol 0.6.11", "postgres-types 0.2.9", "rand 0.9.0", "socket2 0.5.10", @@ -14550,7 +14682,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -14563,7 +14695,7 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "toml_datetime 0.7.0", "toml_parser", "winnow 0.7.15", @@ -14571,11 +14703,11 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.2", ] [[package]] @@ -14594,7 +14726,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -14626,7 +14758,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -14671,7 +14803,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.12.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -14750,11 +14882,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", + "symlink", "thiserror 2.0.18", "time", "tracing-subscriber", @@ -15013,9 +15146,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "typetag" @@ -15184,9 +15317,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -15240,7 +15373,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -15386,7 +15519,7 @@ checksum = "97599c400fc79925922b58303e98fcb8fa88f573379a08ddb652e72cbd2e70f6" dependencies = [ "bitflags 2.9.4", "encoding_rs", - "indexmap 2.12.0", + "indexmap 2.14.0", "num-bigint", "serde", "thiserror 1.0.69", @@ -15471,11 +15604,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -15484,7 +15617,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -15502,6 +15635,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -15606,7 +15740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.12.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -15642,8 +15776,8 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.9.4", "hashbrown 0.15.5", - "indexmap 2.12.0", - "semver 1.0.27", + "indexmap 2.14.0", + "semver 1.0.28", ] [[package]] @@ -15686,14 +15820,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.6", + "webpki-root-certs 1.0.7", ] [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -15704,14 +15838,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -15728,7 +15862,7 @@ dependencies = [ "cfg_aliases 0.1.1", "codespan-reporting", "document-features", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "naga", "once_cell", @@ -15886,7 +16020,7 @@ dependencies = [ [[package]] name = "windmill" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-nats", @@ -15966,7 +16100,7 @@ dependencies = [ [[package]] name = "windmill-ai" -version = "1.688.0" +version = "1.689.0" dependencies = [ "async-trait", "aws-config", @@ -15990,7 +16124,7 @@ dependencies = [ [[package]] name = "windmill-alerting" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16003,7 +16137,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "argon2", @@ -16036,10 +16170,10 @@ dependencies = [ "futures", "git-version", "hex", - "hmac", + "hmac 0.12.1", "http 1.4.0", - "hyper 1.8.1", - "indexmap 2.12.0", + "hyper 1.9.0", + "indexmap 2.14.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", "lazy_static", @@ -16145,12 +16279,12 @@ dependencies = [ [[package]] name = "windmill-api-agent-workers" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "quick_cache", "serde", @@ -16168,7 +16302,7 @@ dependencies = [ [[package]] name = "windmill-api-assets" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16181,7 +16315,7 @@ dependencies = [ [[package]] name = "windmill-api-auth" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16207,7 +16341,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.688.0" +version = "1.689.0" dependencies = [ "reqwest 0.12.28", "serde", @@ -16217,7 +16351,7 @@ dependencies = [ [[package]] name = "windmill-api-configs" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16234,7 +16368,7 @@ dependencies = [ [[package]] name = "windmill-api-debug" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "base64 0.22.1", @@ -16256,7 +16390,7 @@ dependencies = [ [[package]] name = "windmill-api-embeddings" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16279,7 +16413,7 @@ dependencies = [ [[package]] name = "windmill-api-flow-conversations" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16295,11 +16429,11 @@ dependencies = [ [[package]] name = "windmill-api-flows" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", - "hyper 1.8.1", + "hyper 1.9.0", "serde", "serde_json", "sql-builder", @@ -16316,7 +16450,7 @@ dependencies = [ [[package]] name = "windmill-api-groups" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16337,7 +16471,7 @@ dependencies = [ [[package]] name = "windmill-api-inputs" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16351,7 +16485,7 @@ dependencies = [ [[package]] name = "windmill-api-integration-tests" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-nats", @@ -16382,14 +16516,14 @@ dependencies = [ [[package]] name = "windmill-api-jobs" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "axum 0.8.4", "base64 0.22.1", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "serde", "serde_json", @@ -16407,7 +16541,7 @@ dependencies = [ [[package]] name = "windmill-api-npm-proxy" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "flate2", @@ -16425,12 +16559,12 @@ dependencies = [ [[package]] name = "windmill-api-openapi" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "axum 0.8.4", "http 1.4.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "itertools 0.14.0", "lazy_static", "serde", @@ -16447,7 +16581,7 @@ dependencies = [ [[package]] name = "windmill-api-schedule" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16467,13 +16601,13 @@ dependencies = [ [[package]] name = "windmill-api-scripts" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", "futures", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "lazy_static", "quick_cache", @@ -16497,7 +16631,7 @@ dependencies = [ [[package]] name = "windmill-api-settings" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16525,7 +16659,7 @@ dependencies = [ [[package]] name = "windmill-api-sse" -version = "1.688.0" +version = "1.689.0" dependencies = [ "lazy_static", "serde", @@ -16537,14 +16671,14 @@ dependencies = [ [[package]] name = "windmill-api-users" -version = "1.688.0" +version = "1.689.0" dependencies = [ "argon2", "axum 0.8.4", "chrono", "dashmap 6.1.0", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "serde", "serde_json", @@ -16562,7 +16696,7 @@ dependencies = [ [[package]] name = "windmill-api-workers" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", @@ -16576,13 +16710,13 @@ dependencies = [ [[package]] name = "windmill-api-workspaces" -version = "1.688.0" +version = "1.689.0" dependencies = [ "axum 0.8.4", "chrono", "hex", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "magic-crypt", "regex", @@ -16609,7 +16743,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.688.0" +version = "1.689.0" dependencies = [ "chrono", "lazy_static", @@ -16623,7 +16757,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -16642,7 +16776,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.688.0" +version = "1.689.0" dependencies = [ "aes-gcm", "aho-corasick", @@ -16677,9 +16811,9 @@ dependencies = [ "git-version", "globset", "hex", - "hmac", - "hyper 1.8.1", - "indexmap 2.12.0", + "hmac 0.12.1", + "hyper 1.9.0", + "indexmap 2.14.0", "itertools 0.14.0", "jsonwebtoken 8.3.0", "lazy_static", @@ -16706,7 +16840,7 @@ dependencies = [ "reqwest-retry", "rsa", "schemars 0.8.22", - "semver 1.0.27", + "semver 1.0.28", "serde", "serde_json", "serde_yml", @@ -16743,7 +16877,7 @@ dependencies = [ [[package]] name = "windmill-dep-map" -version = "1.688.0" +version = "1.689.0" dependencies = [ "chrono", "itertools 0.14.0", @@ -16762,7 +16896,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.688.0" +version = "1.689.0" dependencies = [ "regex", "serde", @@ -16777,7 +16911,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "astral-tokio-tar", @@ -16801,7 +16935,7 @@ dependencies = [ [[package]] name = "windmill-jseval" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "futures", @@ -16818,7 +16952,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.688.0" +version = "1.689.0" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -16834,7 +16968,7 @@ dependencies = [ [[package]] name = "windmill-mcp" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -16855,7 +16989,7 @@ dependencies = [ [[package]] name = "windmill-native-triggers" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -16863,7 +16997,7 @@ dependencies = [ "backon", "base64 0.22.1", "chrono", - "hmac", + "hmac 0.12.1", "http 1.4.0", "itertools 0.14.0", "lazy_static", @@ -16886,7 +17020,7 @@ dependencies = [ [[package]] name = "windmill-oauth" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "arc-swap", @@ -16895,7 +17029,7 @@ dependencies = [ "base64 0.22.1", "chrono", "hex", - "hmac", + "hmac 0.12.1", "itertools 0.14.0", "lazy_static", "reqwest 0.12.28", @@ -16911,7 +17045,7 @@ dependencies = [ [[package]] name = "windmill-object-store" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-stream", @@ -16945,7 +17079,7 @@ dependencies = [ [[package]] name = "windmill-operator" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "futures", @@ -16963,7 +17097,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.688.0" +version = "1.689.0" dependencies = [ "convert_case 0.6.0", "serde", @@ -16972,7 +17106,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -16984,7 +17118,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde_json", @@ -16996,7 +17130,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "gosyn", @@ -17008,7 +17142,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -17020,7 +17154,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde_json", @@ -17032,7 +17166,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "nu-parser", @@ -17043,7 +17177,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -17054,7 +17188,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -17066,7 +17200,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "rustpython-ast", @@ -17077,7 +17211,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-recursion", @@ -17099,7 +17233,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde_json", @@ -17111,7 +17245,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -17125,7 +17259,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -17142,7 +17276,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -17155,7 +17289,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde", @@ -17167,7 +17301,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -17185,7 +17319,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -17201,7 +17335,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "rustpython-ast", @@ -17217,7 +17351,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde", @@ -17228,7 +17362,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-recursion", @@ -17241,7 +17375,7 @@ dependencies = [ "futures", "futures-core", "hex", - "hmac", + "hmac 0.12.1", "itertools 0.14.0", "lazy_static", "once_cell", @@ -17265,7 +17399,7 @@ dependencies = [ [[package]] name = "windmill-runtime-nativets" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "const_format", @@ -17303,7 +17437,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.688.0" +version = "1.689.0" dependencies = [ "getrandom 0.3.4", "wasm-bindgen", @@ -17314,7 +17448,7 @@ dependencies = [ [[package]] name = "windmill-store" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-recursion", @@ -17322,7 +17456,7 @@ dependencies = [ "chrono", "futures", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "lazy_static", "quick_cache", "reqwest 0.13.1", @@ -17344,7 +17478,7 @@ dependencies = [ [[package]] name = "windmill-test-utils" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17368,14 +17502,14 @@ dependencies = [ [[package]] name = "windmill-trigger" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", "axum 0.8.4", "chrono", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "lazy_static", "rand 0.9.0", @@ -17401,7 +17535,7 @@ dependencies = [ [[package]] name = "windmill-trigger-email" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17421,7 +17555,7 @@ dependencies = [ [[package]] name = "windmill-trigger-gcp" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17455,7 +17589,7 @@ dependencies = [ [[package]] name = "windmill-trigger-http" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17465,9 +17599,9 @@ dependencies = [ "constant_time_eq 0.3.1", "futures", "hex", - "hmac", + "hmac 0.12.1", "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "itertools 0.14.0", "lazy_static", "matchit 0.7.3", @@ -17491,7 +17625,7 @@ dependencies = [ [[package]] name = "windmill-trigger-kafka" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17514,7 +17648,7 @@ dependencies = [ [[package]] name = "windmill-trigger-mqtt" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17538,7 +17672,7 @@ dependencies = [ [[package]] name = "windmill-trigger-nats" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-nats", @@ -17562,7 +17696,7 @@ dependencies = [ [[package]] name = "windmill-trigger-postgres" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17597,7 +17731,7 @@ dependencies = [ [[package]] name = "windmill-trigger-sqs" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17625,7 +17759,7 @@ dependencies = [ [[package]] name = "windmill-trigger-websocket" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-trait", @@ -17648,7 +17782,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "bitflags 2.9.4", @@ -17667,7 +17801,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-once-cell", @@ -17695,7 +17829,7 @@ dependencies = [ "gcp_auth", "git-version", "hex", - "hmac", + "hmac 0.12.1", "hudsucker", "hyper-http-proxy", "hyper-tls", @@ -17779,7 +17913,7 @@ dependencies = [ [[package]] name = "windmill-worker-volumes" -version = "1.688.0" +version = "1.689.0" dependencies = [ "bytes", "futures", @@ -18379,9 +18513,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "winsafe" @@ -18398,6 +18532,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -18417,7 +18557,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -18448,7 +18588,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.9.4", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -18467,9 +18607,9 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", - "semver 1.0.27", + "semver 1.0.28", "serde", "serde_derive", "serde_json", @@ -18479,9 +18619,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wtf8" @@ -18614,12 +18754,12 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.1", + "yoke-derive 0.8.2", "zerofrom", ] @@ -18637,9 +18777,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -18649,18 +18789,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -18669,18 +18809,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -18710,31 +18850,31 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -18748,7 +18888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" dependencies = [ "crc32fast", - "indexmap 2.12.0", + "indexmap 2.14.0", "memchr", "typed-path", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index ca9dc25e9b..8115b10955 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.688.0" +version = "1.689.0" authors.workspace = true edition.workspace = true @@ -86,7 +86,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"] [workspace.package] -version = "1.688.0" +version = "1.689.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock index 00875379fb..a4a4b4afaa 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.lock +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill-common" -version = "1.688.0" +version = "1.689.0" dependencies = [ "aho-corasick", "anyhow", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.688.0" +version = "1.689.0" dependencies = [ "proc-macro2", "quote", @@ -6275,7 +6275,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.688.0" +version = "1.689.0" dependencies = [ "convert_case", "serde", @@ -6284,7 +6284,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -6296,7 +6296,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde_json", @@ -6308,7 +6308,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "gosyn", @@ -6320,7 +6320,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -6332,7 +6332,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde_json", @@ -6344,7 +6344,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "nu-parser", @@ -6355,7 +6355,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6366,7 +6366,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "itertools 0.14.0", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-asset" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6389,7 +6389,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "async-recursion", @@ -6411,7 +6411,7 @@ dependencies = [ [[package]] name = "windmill-parser-r" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde_json", @@ -6423,7 +6423,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -6437,7 +6437,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "convert_case", @@ -6454,7 +6454,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -6467,7 +6467,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql-asset" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "lazy_static", @@ -6497,7 +6497,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts-asset" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde-wasm-bindgen", @@ -6513,7 +6513,7 @@ dependencies = [ [[package]] name = "windmill-parser-wac" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "rustpython-ast", @@ -6529,7 +6529,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -6561,7 +6561,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "serde", @@ -6572,7 +6572,7 @@ dependencies = [ [[package]] name = "windmill-types" -version = "1.688.0" +version = "1.689.0" dependencies = [ "anyhow", "bitflags", diff --git a/backend/parsers/windmill-parser-wasm/Cargo.toml b/backend/parsers/windmill-parser-wasm/Cargo.toml index a99de1d8fe..84d0c7ef7c 100644 --- a/backend/parsers/windmill-parser-wasm/Cargo.toml +++ b/backend/parsers/windmill-parser-wasm/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" members = ["."] [workspace.package] -version = "1.688.0" +version = "1.689.0" edition = "2021" authors = ["Ruben Fiszel "] diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5f688d23b2..5993f59bfe 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.688.0 + version: 1.689.0 title: Windmill API contact: diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 3d050361c2..09577c1a0d 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.688.0"; +export const VERSION = "v1.689.0"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index 96630f81ff..5172fc12c8 100755 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -78,7 +78,7 @@ export { token, }; -export const VERSION = "1.688.0"; +export const VERSION = "1.689.0"; // Re-exported from constants.ts to maintain backwards compatibility export { WM_FORK_PREFIX } from "./core/constants.ts"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 21885edc01..a3a19bd739 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.688.0", + "version": "1.689.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.688.0", + "version": "1.689.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -844,6 +844,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -855,6 +856,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -865,6 +867,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1354,6 +1357,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1510,6 +1514,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1526,6 +1531,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1542,6 +1548,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1558,6 +1565,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1574,6 +1582,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1590,6 +1599,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1606,6 +1616,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1622,6 +1633,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1638,6 +1650,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1654,6 +1667,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1670,6 +1684,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1686,6 +1701,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1702,6 +1718,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1718,6 +1735,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1734,6 +1752,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2039,6 +2058,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6814,7 +6834,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7313,6 +7333,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7333,6 +7354,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7353,6 +7375,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7373,6 +7396,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7393,6 +7417,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7413,6 +7438,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7433,6 +7459,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7453,6 +7480,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7473,6 +7501,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7493,6 +7522,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7513,6 +7543,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12081,6 +12112,21 @@ } } }, + "node_modules/svelte-check/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -12811,7 +12857,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index a09dcf7c8f..4cc789da2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.688.0", + "version": "1.689.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/lsp/Pipfile b/lsp/Pipfile index e04f65b74d..8c35ff5016 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.688.0" +wmill = ">=1.689.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index d575c2ca3b..0463212b83 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: '3.0.3' info: - version: 1.688.0 + version: 1.689.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index f5679bf04e..48b63f68c5 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.688.0' + ModuleVersion = '1.689.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 82b4c639b4..89d0fa8477 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.688.0" +version = "1.689.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 156f34b2cc..465f435ec9 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.688.0", + "version": "1.689.0", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index c4d1ffb180..98b09808cb 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.688.0", + "version": "1.689.0", "author": "Ruben Fiszel", "license": "Apache 2.0", "sideEffects": false, diff --git a/version.txt b/version.txt index 7ede926357..4078a2ea87 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.688.0 +1.689.0 From 9a60ff2e77f197786f523755c3a9286a178a245c Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:13:20 +0200 Subject: [PATCH 14/28] feat: add ai agent conversation output control (#8915) * feat: add ai agent chat output flag Co-Authored-By: Claude Opus 4.5 * fix: suppress ai agent tool chat messages Co-Authored-By: Claude Opus 4.5 * refactor: rename ai agent conversation output flag Co-Authored-By: Claude Opus 4.5 * feat: expose ai agent conversation output toggle Co-Authored-By: Claude Opus 4.5 * fix: gate ai agent chat tab by chat mode Co-Authored-By: Claude Opus 4.5 * chore: regenerate system prompts Co-Authored-By: Claude Opus 4.5 * fix: address ai agent chat review feedback Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- backend/windmill-types/src/flows.rs | 43 ++++++++++++++ backend/windmill-worker/src/ai/tools.rs | 5 ++ backend/windmill-worker/src/ai_executor.rs | 14 +++-- backend/windmill-worker/src/worker_flow.rs | 21 ++++++- .../windmill-worker/src/worker_lockfiles.rs | 13 ++++- cli/src/guidance/skills.ts | 2 +- .../flows/content/FlowModuleComponent.svelte | 56 +++++++++++++++++-- openflow.openapi.yaml | 4 ++ system_prompts/auto-generated/flow.md | 2 +- system_prompts/auto-generated/prompts.ts | 2 +- .../auto-generated/skills/write-flow/SKILL.md | 2 +- 11 files changed, 149 insertions(+), 15 deletions(-) diff --git a/backend/windmill-types/src/flows.rs b/backend/windmill-types/src/flows.rs index 14bcf6d8d5..8b60eb0d44 100644 --- a/backend/windmill-types/src/flows.rs +++ b/backend/windmill-types/src/flows.rs @@ -63,6 +63,10 @@ pub fn is_none_or_false(b: &Option) -> bool { b.is_none() || !b.unwrap() } +fn is_false(b: &bool) -> bool { + !*b +} + #[derive(Serialize, sqlx::FromRow)] pub struct ListableFlow { pub workspace_id: String, @@ -921,6 +925,8 @@ pub enum FlowModuleValue { AIAgent { input_transforms: HashMap, tools: Vec, + #[serde(default, skip_serializing_if = "is_false")] + omit_output_from_conversation: bool, }, } @@ -955,6 +961,7 @@ struct UntaggedFlowModuleValue { modules_node: Option, assets: Option>, tools: Option>, + omit_output_from_conversation: Option, pass_flow_input_directly: Option, squash: Option, #[serde(flatten)] @@ -1056,6 +1063,9 @@ impl<'de> Deserialize<'de> for FlowModuleValue { tools: untagged .tools .ok_or_else(|| serde::de::Error::missing_field("tools"))?, + omit_output_from_conversation: untagged + .omit_output_from_conversation + .unwrap_or(false), }), other => Err(serde::de::Error::unknown_variant( other, @@ -1173,4 +1183,37 @@ mod tests { let val: FlowValue = serde_json::from_value(input).unwrap(); assert_eq!(val.modules.len(), 1); } + + #[test] + fn ai_agent_omit_output_from_conversation_defaults_to_false() { + let input = json!({ + "type": "aiagent", + "tools": [], + "input_transforms": {} + }); + + let val: FlowModuleValue = serde_json::from_value(input).unwrap(); + let FlowModuleValue::AIAgent { omit_output_from_conversation, .. } = val else { + panic!("expected aiagent module"); + }; + + assert!(!omit_output_from_conversation); + } + + #[test] + fn ai_agent_omit_output_from_conversation_preserves_true() { + let input = json!({ + "type": "aiagent", + "tools": [], + "input_transforms": {}, + "omit_output_from_conversation": true + }); + + let val: FlowModuleValue = serde_json::from_value(input).unwrap(); + let FlowModuleValue::AIAgent { omit_output_from_conversation, .. } = val else { + panic!("expected aiagent module"); + }; + + assert!(omit_output_from_conversation); + } } diff --git a/backend/windmill-worker/src/ai/tools.rs b/backend/windmill-worker/src/ai/tools.rs index a58cab06fd..54b3bf6fa3 100644 --- a/backend/windmill-worker/src/ai/tools.rs +++ b/backend/windmill-worker/src/ai/tools.rs @@ -75,6 +75,7 @@ pub struct ToolExecutionContext<'a> { // Optional streaming & chat pub stream_event_processor: Option<&'a StreamEventProcessor>, pub flow_context: &'a mut FlowContext, + pub omit_output_from_conversation: bool, pub previous_result: &'a Option>, pub id_context: &'a Option, @@ -780,6 +781,10 @@ async fn add_tool_message_to_chat( content: &str, success: bool, ) { + if ctx.omit_output_from_conversation { + return; + } + let chat_enabled = ctx .flow_context .flow_status diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index 2fcb86f3de..03153df0a4 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -275,7 +275,9 @@ pub async fn handle_ai_agent_job( let summary = module.summary.clone(); - let FlowModuleValue::AIAgent { tools, .. } = module.get_value()? else { + let FlowModuleValue::AIAgent { tools, omit_output_from_conversation, .. } = + module.get_value()? + else { return Err(Error::internal_err( "AI agent module is not an AI agent".to_string(), )); @@ -504,6 +506,7 @@ pub async fn handle_ai_agent_job( killpill_rx, has_stream, has_websearch, + omit_output_from_conversation, cancel_rx, tool_abort_handles.clone(), ); @@ -600,6 +603,7 @@ pub async fn run_agent( killpill_rx: &mut tokio::sync::broadcast::Receiver<()>, has_stream: &mut bool, has_websearch: bool, + omit_output_from_conversation: bool, // cancellation signal from parent cancel_rx: tokio::sync::watch::Receiver, @@ -860,6 +864,7 @@ pub async fn run_agent( .as_ref() .and_then(|fs| fs.chat_input_enabled) .unwrap_or(false); + let persist_output_to_conversation = chat_enabled && !omit_output_from_conversation; let step_name = get_step_name_from_flow(summary.as_deref(), effective_flow_step_id); @@ -1061,7 +1066,7 @@ pub async fn run_agent( agent_action: Some(AgentAction::WebSearch {}), ..Default::default() }); - if chat_enabled { + if persist_output_to_conversation { if let Some(memory_id) = memory_id { let agent_job_id = job.id; let db_clone = db.clone(); @@ -1113,7 +1118,7 @@ pub async fn run_agent( content = Some(OpenAIContent::Text(response_content.clone())); // Add assistant message to conversation if chat_input_enabled - if chat_enabled && !response_content.is_empty() { + if persist_output_to_conversation && !response_content.is_empty() { if let Some(memory_id) = memory_id { let agent_job_id = job.id; let db_clone = db.clone(); @@ -1195,6 +1200,7 @@ pub async fn run_agent( killpill_rx, stream_event_processor: stream_event_processor.as_ref(), flow_context: &mut flow_context, + omit_output_from_conversation, previous_result: &previous_result, id_context: &id_context, tool_abort_handles: tool_abort_handles.clone(), @@ -1230,7 +1236,7 @@ pub async fn run_agent( let content = to_raw_value(&s3_object); // Add assistant message to conversation if chat_input_enabled - if chat_enabled { + if persist_output_to_conversation { if let Some(memory_id) = memory_id { let agent_job_id = job.id; let db_clone = db.clone(); diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 1abe2a75d9..dfbf6739dd 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -405,6 +405,7 @@ pub async fn update_flow_status_after_job_completion_internal( chat_input_enabled: bool, conversation_id: Option, is_ai_agent_step: bool, + omit_output_from_conversation: bool, } let ( should_continue_flow, @@ -1609,10 +1610,22 @@ pub async fn update_flow_status_after_job_completion_internal( current_module_id = %current_module.map(|x| x.id.clone()).unwrap_or_default(), continue_on_error = %continue_on_error, should_continue_flow = %should_continue_flow, "computed if flow should continue"); + let is_ai_agent_step = current_module.is_some_and(|m| m.is_ai_agent()); + let omit_output_from_conversation = match (is_ai_agent_step, current_module) { + (true, Some(module)) => match module.get_value()? { + FlowModuleValue::AIAgent { omit_output_from_conversation, .. } => { + omit_output_from_conversation + } + _ => false, + }, + _ => false, + }; + let chat_ai_info = ChatAiInfo { chat_input_enabled: old_status.chat_input_enabled.unwrap_or(false), conversation_id: old_status.memory_id, - is_ai_agent_step: current_module.is_some_and(|m| m.is_ai_agent()), + is_ai_agent_step, + omit_output_from_conversation, }; ( should_continue_flow, @@ -1843,6 +1856,7 @@ pub async fn update_flow_status_after_job_completion_internal( success, skipped, chat_ai_info.is_ai_agent_step, + chat_ai_info.omit_output_from_conversation, &nresult, chat_ai_info.chat_input_enabled, chat_ai_info.conversation_id, @@ -2035,10 +2049,15 @@ async fn add_tool_message_to_conversation( success: bool, skipped: bool, is_ai_agent_step: bool, + omit_output_from_conversation: bool, result: &Box, chat_input_enabled: bool, conversation_id: Option, ) -> error::Result<()> { + if is_ai_agent_step && omit_output_from_conversation { + return Ok(()); + } + // Create assistant message if it's a flow and it's done, but only if last module is not an AI agent if !skipped && chat_input_enabled { // Get conversation_id from flow_status.memory_id diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index a6b062b337..407efbb717 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -1184,7 +1184,11 @@ async fn lock_modules<'c>( .execute(&mut *tx) .await?; } - FlowModuleValue::AIAgent { input_transforms, mut tools } => { + FlowModuleValue::AIAgent { + input_transforms, + mut tools, + omit_output_from_conversation, + } => { // Extract FlowModules from tools and track their original indices // MCP tools don't need locking, so we filter them out let mut flow_modules = Vec::new(); @@ -1232,7 +1236,12 @@ async fn lock_modules<'c>( tools[idx] = locked.into(); } - e.value = FlowModuleValue::AIAgent { input_transforms, tools }.into(); + e.value = FlowModuleValue::AIAgent { + input_transforms, + tools, + omit_output_from_conversation, + } + .into(); } _ => (), }; diff --git a/cli/src/guidance/skills.ts b/cli/src/guidance/skills.ts index 7831fefe0e..6f2f7d100d 100644 --- a/cli/src/guidance/skills.ts +++ b/cli/src/guidance/skills.ts @@ -4659,7 +4659,7 @@ Reference a specific resource using \`$res:\` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, "raw-app": `--- name: raw-app description: MUST use when creating raw apps. diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index 2ff6968ee9..ad818440ec 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -156,6 +156,12 @@ }) let selected = $state(untrack(() => preprocessorModule) ? 'test' : 'inputs') + let canShowChatTab = $derived( + !preprocessorModule && + Boolean(flowStore.val.value?.chat_input_enabled) && + flowModule.value.type === 'aiagent' + ) + let visibleSelected = $derived(selected === 'chat' && !canShowChatTab ? 'inputs' : selected) let advancedSelected = $state('retries') let advancedRuntimeSelected = $state('concurrency') let s3Kind = $state('s3_client') @@ -239,6 +245,18 @@ advancedSelected = subtab } + function setOmitOutputFromConversation(omit: boolean) { + if (flowModule.value.type !== 'aiagent') { + return + } + + if (omit) { + flowModule.value.omit_output_from_conversation = true + } else { + delete flowModule.value.omit_output_from_conversation + } + } + let forceReload = $state(0) let editorPanelSize = $state( untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50 @@ -1021,16 +1039,29 @@
- + { + selected = event.detail + }} + wrapperClass="shrink-0" + > {#if !preprocessorModule} {/if} + {#if canShowChatTab && flowModule.value.type === 'aiagent'} + + {/if} {#if !preprocessorModule && !isAgentTool} {/if} - {#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow' || flowModule.value.type == 'aiagent')} + {#if visibleSelected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow' || flowModule.value.type == 'aiagent')}
- {:else if selected === 'test'} + {:else if visibleSelected === 'test'} {#if debugMode && isDebuggableScript}
- {:else if selected === 'advanced'} + {:else if visibleSelected === 'chat' && canShowChatTab && flowModule.value.type === 'aiagent'} +
+
+ { + setOmitOutputFromConversation(event.detail) + }} + options={{ + right: 'Omit assistant and tool messages from the flow conversation', + rightTooltip: + 'When enabled, this AI agent still runs normally, but its assistant response and tool-use messages are not stored in chat-mode conversation history.' + }} + /> +
+
+ {:else if visibleSelected === 'advanced'} ' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index 29050a21e4..b7d525add4 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -1755,7 +1755,7 @@ class SqlQuery: export const OPENFLOW_SCHEMA = `## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`; +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`; export const CLI_COMMANDS = `# Windmill CLI Commands diff --git a/system_prompts/auto-generated/skills/write-flow/SKILL.md b/system_prompts/auto-generated/skills/write-flow/SKILL.md index 0978d8fd60..1d1398f888 100644 --- a/system_prompts/auto-generated/skills/write-flow/SKILL.md +++ b/system_prompts/auto-generated/skills/write-flow/SKILL.md @@ -310,4 +310,4 @@ Reference a specific resource using `$res:` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \"yellow\", \"#ffff00\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/-"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\nValid values: 'text' (default) - plain text response, 'image' - image generation\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\nFormat: Array<{ bucket: string, key: string }> - S3 object references\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\nRange: 0.0 to 2.0 (provider-dependent)\n- 0.0 = deterministic, focused responses\n- 0.7 = balanced (common default)\n- 1.0+ = more creative/random\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}} \ No newline at end of file From dac29e7d23d6e980c2b6fc4dd3a05a0d2e0170b3 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 23 Apr 2026 08:36:56 -0700 Subject: [PATCH 15/28] fix: load job metadata on approval page via approval token (#8924) * fix: load job metadata on approval page via approval token The approval page polled getJob without auth, which 400s for non-anonymous jobs. The page swallowed the error so approvers saw the form but no flow args, metadata, or graph. Accept the existing approval token on getJob and skip the non-anon-user check when it validates against the job's flow. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(approval): address review feedback - Validate approval token against URL job id directly before resolving the parent flow, saving a DB roundtrip on the happy path (approval URLs always carry the flow id). - Request getJob with no_code/no_logs from the approval page so a token-bearer only sees what the UI renders (args, raw_flow, metadata). - Tighten OpenAPI description for the approval_token query param. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- backend/windmill-api/openapi.yaml | 5 ++++ backend/windmill-api/src/jobs.rs | 29 ++++++++++++++++--- .../approve/[workspace]/[job]/+page.svelte | 5 +++- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 5993f59bfe..2f8567a9f6 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -11687,6 +11687,11 @@ paths: in: query schema: type: boolean + - name: approval_token + in: query + description: Approval token granting read access to the job when not logged in. The token must be the one issued for this job's flow (i.e. the flow id used when generating the approval URL). + schema: + type: string responses: "200": description: job details diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 096aaeb969..984761510b 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -935,6 +935,7 @@ async fn list_selected_job_groups( struct GetJobQuery { pub no_logs: Option, pub no_code: Option, + pub approval_token: Option, } async fn get_job( @@ -942,16 +943,36 @@ async fn get_job( opt_tokened: OptTokened, Extension(db): Extension, Path((w_id, id)): Path<(String, Uuid)>, - Query(GetJobQuery { no_logs, no_code }): Query, + Query(GetJobQuery { no_logs, no_code, approval_token }): Query, ) -> error::Result { let tags = opt_authed .as_ref() .map(|authed| get_scope_tags(authed)) .flatten(); - let mut get = GetQuery::new() - .with_auth(&opt_authed) - .with_in_tags(tags.as_ref()); + // A valid approval token on the same (workspace, flow) grants read access + // so the approval page can render job metadata without login. The approval + // URL usually carries the flow id directly — try that first and only + // resolve the parent flow if the direct check fails. + let has_valid_approval_token = if let Some(ref token) = approval_token { + if validate_approval_token(&db, token, id, &w_id).await.is_ok() { + true + } else if let Ok(flow_id) = get_flow_id_for_job(&db, id).await { + flow_id != id + && validate_approval_token(&db, token, flow_id, &w_id) + .await + .is_ok() + } else { + false + } + } else { + false + }; + + let mut get = GetQuery::new().with_in_tags(tags.as_ref()); + if !has_valid_approval_token { + get = get.with_auth(&opt_authed); + } if no_code.unwrap_or(false) { get = get.without_code(); diff --git a/frontend/src/routes/approve/[workspace]/[job]/+page.svelte b/frontend/src/routes/approve/[workspace]/[job]/+page.svelte index 4055b4e62b..9ecdcaebb5 100644 --- a/frontend/src/routes/approve/[workspace]/[job]/+page.svelte +++ b/frontend/src/routes/approve/[workspace]/[job]/+page.svelte @@ -79,7 +79,10 @@ try { job = (await JobService.getJob({ workspace: page.params.workspace ?? '', - id: page.params.job ?? '' + id: page.params.job ?? '', + approvalToken: token, + noCode: true, + noLogs: true })) as Job completed = job?.type === 'CompletedJob' if (completed) { From 2ed26c2254d41aeb7339268c66c00496afbd5a51 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 23 Apr 2026 16:16:10 +0000 Subject: [PATCH 16/28] rust client nit --- rust-client/src/client.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust-client/src/client.rs b/rust-client/src/client.rs index 23c1b5b065..5cf152f520 100644 --- a/rust-client/src/client.rs +++ b/rust-client/src/client.rs @@ -866,6 +866,7 @@ impl Windmill { job_id, Some(true), Some(true), + None, ) .await?; @@ -973,6 +974,7 @@ impl Windmill { &job_id, Some(true), Some(true), + None, ) .await?; From 07951e81ae9a1c26e8fe63bcd7a760b80500ca4c Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:21:15 +0200 Subject: [PATCH 17/28] fix: include endpoint descriptions in mcp tools (#8925) Co-authored-by: Claude Opus 4.5 --- .../generate_mcp_tools.py | 17 ++++- backend/windmill-api/openapi.yaml | 3 + .../src/mcp/auto_generated_endpoints.rs | 65 +++++++++++++++++-- frontend/src/lib/mcpEndpointTools.ts | 64 ++++++++++++++++-- 4 files changed, 139 insertions(+), 10 deletions(-) diff --git a/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py b/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py index 7bcf125b8e..b120a00d39 100644 --- a/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py +++ b/backend/generate_mcp_endpoints_tools/generate_mcp_tools.py @@ -384,6 +384,19 @@ def schema_to_rust_value(schema: Optional[Dict[str, Any]]) -> str: return "None" return f"Some(serde_json::json!({json.dumps(schema, indent=8)}))" +def build_tool_description(operation: Dict[str, Any], method: str, path: str) -> str: + """Build the MCP tool description from OpenAPI summary and description.""" + summary = operation.get('summary', '').strip() + description = operation.get('description', '').strip() + + if summary and description: + return f"{summary}: {description}".rstrip('.!? ') + if summary: + return summary + if description: + return description.rstrip('.!? ') + return f'{method.upper()} {path}' + def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]: """Find all endpoints marked with x-mcp-tool: true.""" tools = [] @@ -395,7 +408,7 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]: # Extract tool information tool = { 'name': operation.get('operationId', f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"), - 'description': operation.get('summary', operation.get('description', f'{method.upper()} {path}')), + 'description': build_tool_description(operation, method, path), 'instructions': operation.get('x-mcp-instructions', ''), 'path': path, 'method': method.upper(), @@ -607,4 +620,4 @@ def main(): print("Done!") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 2f8567a9f6..17127ef3d6 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -7406,6 +7406,9 @@ paths: /w/{workspace}/scripts/create: post: summary: create script + description: | + Creates a new script when the path does not already exist. + Creates a new version of an existing script when called with the same path and the current `parent_hash`. operationId: createScript x-mcp-tool: true x-mcp-instructions: "To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed." diff --git a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs index c9295ea8e9..fc3b1164f5 100644 --- a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs +++ b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs @@ -78,6 +78,12 @@ pub fn all_tools() -> Vec { "type": "string", "description": "The expiration date of the variable", "format": "date-time" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -157,6 +163,12 @@ pub fn all_tools() -> Vec { "type": "string", "description": "The new description of the variable" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, "path__body": { "type": "string", "description": "The path to the variable (body parameter)" @@ -244,6 +256,10 @@ pub fn all_tools() -> Vec { "per_page": { "type": "integer", "description": "number of items to return for a given page (default 30, max 100)" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -287,6 +303,12 @@ pub fn all_tools() -> Vec { "resource_type": { "type": "string", "description": "The resource_type associated with the resource" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -355,6 +377,12 @@ pub fn all_tools() -> Vec { "type": "string", "description": "The new resource_type to be associated with the resource" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, "path__body": { "type": "string", "description": "The path to the resource (body parameter)" @@ -437,6 +465,10 @@ pub fn all_tools() -> Vec { "broad_filter": { "type": "string", "description": "broad search across multiple fields (case-insensitive substring match)" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -544,6 +576,10 @@ pub fn all_tools() -> Vec { "dedicated_worker": { "type": "boolean", "description": "(default regardless)\nIf true, show only scripts with dedicated_worker enabled.\nIf false, show only scripts with dedicated_worker disabled.\n" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -555,7 +591,8 @@ pub fn all_tools() -> Vec { }, EndpointTool { name: Cow::Borrowed("createScript"), - description: Cow::Borrowed("create script"), + description: Cow::Borrowed("create script: Creates a new script when the path does not already exist. +Creates a new version of an existing script when called with the same path and the current `parent_hash`"), instructions: Cow::Borrowed("To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed."), path: Cow::Borrowed("/w/{workspace}/scripts/create"), method: Cow::Borrowed("POST"), @@ -578,7 +615,7 @@ pub fn all_tools() -> Vec { }, "language": { "type": "string", - "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative" }, "kind": { "type": "string", @@ -772,6 +809,10 @@ pub fn all_tools() -> Vec { "dedicated_worker": { "type": "boolean", "description": "(default regardless)\nIf true, show only flows with dedicated_worker enabled.\nIf false, show only flows with dedicated_worker disabled.\n" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -1095,7 +1136,7 @@ pub fn all_tools() -> Vec { }, "language": { "type": "string", - "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative" }, "tag": { "type": "string" @@ -1127,7 +1168,7 @@ pub fn all_tools() -> Vec { }, "language": { "type": "string", - "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative" }, "lock": { "type": "string", @@ -1691,6 +1732,12 @@ You should get the schema of the script or flow before creating the schedule to "preserve_permissioned_as": { "type": "boolean", "description": "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it." + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -1894,6 +1941,12 @@ You should get the schema of the script or flow before updating the schedule to "type": "boolean", "nullable": true, "description": "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -2001,6 +2054,10 @@ You should get the schema of the script or flow before updating the schedule to "broad_filter": { "type": "string", "description": "broad search across multiple fields (case-insensitive substring match)" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] diff --git a/frontend/src/lib/mcpEndpointTools.ts b/frontend/src/lib/mcpEndpointTools.ts index e60103a65a..b86119477e 100644 --- a/frontend/src/lib/mcpEndpointTools.ts +++ b/frontend/src/lib/mcpEndpointTools.ts @@ -88,6 +88,12 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string", "description": "The expiration date of the variable", "format": "date-time" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -167,6 +173,12 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string", "description": "The new description of the variable" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, "path__body": { "type": "string", "description": "The path to the variable (body parameter)" @@ -254,6 +266,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "per_page": { "type": "integer", "description": "number of items to return for a given page (default 30, max 100)" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -297,6 +313,12 @@ export const mcpEndpointTools: EndpointTool[] = [ "resource_type": { "type": "string", "description": "The resource_type associated with the resource" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -365,6 +387,12 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string", "description": "The new resource_type to be associated with the resource" }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + }, "path__body": { "type": "string", "description": "The path to the resource (body parameter)" @@ -447,6 +475,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "broad_filter": { "type": "string", "description": "broad search across multiple fields (case-insensitive substring match)" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -554,6 +586,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "dedicated_worker": { "type": "boolean", "description": "(default regardless)\nIf true, show only scripts with dedicated_worker enabled.\nIf false, show only scripts with dedicated_worker disabled.\n" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -565,7 +601,7 @@ export const mcpEndpointTools: EndpointTool[] = [ }, { name: "createScript", - description: "create script", + description: "create script: Creates a new script when the path does not already exist.\nCreates a new version of an existing script when called with the same path and the current `parent_hash`", instructions: "To create a script, specify the path (e.g., 'f/my_folder/my_script'), the content (source code), and the language. For TypeScript, use 'bun' unless deno-specific APIs are needed.", path: "/w/{workspace}/scripts/create", method: "POST", @@ -588,7 +624,7 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "language": { "type": "string", - "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative" }, "kind": { "type": "string", @@ -782,6 +818,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "dedicated_worker": { "type": "boolean", "description": "(default regardless)\nIf true, show only flows with dedicated_worker enabled.\nIf false, show only flows with dedicated_worker disabled.\n" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] @@ -1105,7 +1145,7 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "language": { "type": "string", - "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative" }, "tag": { "type": "string" @@ -1137,7 +1177,7 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "language": { "type": "string", - "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, duckdb, bunnative" + "description": "Possible values: python3, deno, go, bash, powershell, postgresql, mysql, bigquery, snowflake, mssql, oracledb, graphql, nativets, bun, php, rust, ansible, csharp, nu, java, ruby, rlang, duckdb, bunnative" }, "lock": { "type": "string", @@ -1698,6 +1738,12 @@ export const mcpEndpointTools: EndpointTool[] = [ "preserve_permissioned_as": { "type": "boolean", "description": "When true and the caller is a member of the 'wm_deployers' group, preserves the original permissioned_as value instead of overwriting it." + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -1898,6 +1944,12 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "boolean", "nullable": true, "description": "If true and user is admin/wm_deployers, preserve the provided permissioned_as instead of using the deploying user's identity" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -2005,6 +2057,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "broad_filter": { "type": "string", "description": "broad search across multiple fields (case-insensitive substring match)" + }, + "label": { + "type": "string", + "description": "Filter by label" } }, "required": [] From 132d8a61f9c109b2b447fab1a39565f52864b746 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:21:58 +0200 Subject: [PATCH 18/28] fix: slim app ai chat context (#8922) * fix: slim app ai chat context Co-Authored-By: Claude Opus 4.5 * fix: remove stale app chat selection UI Co-Authored-By: Claude Opus 4.5 * refactor: trim app chat selected context Co-Authored-By: Claude Opus 4.5 * test: trim app chat context coverage Co-Authored-By: Claude Opus 4.5 * test: remove app tool assertion Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .../copilot/chat/AIChatDisplay.svelte | 41 +----- .../components/copilot/chat/app/core.test.ts | 115 +++++++++++++++- .../lib/components/copilot/chat/app/core.ts | 127 +----------------- .../components/raw_apps/RawAppEditor.svelte | 39 +----- 4 files changed, 124 insertions(+), 198 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte index 140f20f140..9081283d09 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte @@ -3,8 +3,6 @@ import { type Snippet } from 'svelte' import { CheckIcon, - Code2, - FileCode, HistoryIcon, Loader2, MousePointer2, @@ -143,7 +141,7 @@
No history
{:else}
- {#each pastChats as chat} + {#each pastChats as chat (chat.id)} -
- {:else if appContext.type === 'backend' && appContext.backendKey && !appContext.selectionExcluded} -
- - {appContext.backendKey} - -
- {/if} + {#if aiChatManager.mode === AIMode.APP && appContext && (appContext.inspectorElement || appContext.codeSelection)} {#if appContext.inspectorElement}
0 && messages.filter((m) => m.role === 'user').length === 0 && !disabled}
- {#each suggestions as suggestion} + {#each suggestions as suggestion (suggestion)} ', + textContent: 'Save', + styles: {} + }, + codeSelection: { + type: 'app_code_selection', + source: '/index.tsx', + sourceType: 'frontend', + title: '/index.tsx:3-4', + content: 'const selectedCode = true', + startLine: 3, + endLine: 4, + startColumn: 1, + endColumn: 25 + } + } as unknown as SelectedContext + + const message = prepareAppUserMessage('Change this selected area', selectedContext) + + const content = message.content as string + expect(content).toContain('The user has selected an element in the app preview') + expect(content).toContain('body > button.primary') + expect(content).toContain('### CODE SELECTION:') + expect(content).toContain('const selectedCode = true') + }) + + it('serializes explicit mentions with lightweight file context', () => { + const additionalContext: ContextElement[] = [ + { + type: 'app_frontend_file', + path: '/index.tsx', + title: '/index.tsx', + content: 'const fullFrontendContent = true' + }, + { + type: 'app_backend_runnable', + key: 'loadUsers', + title: 'loadUsers', + runnable: { + name: 'Load users', + type: 'inline', + staticInputs: { admin: true }, + inlineScript: { + language: 'bun', + content: 'export async function main() { return "secret" }' + } + } + }, + { + type: 'app_datatable', + datatableName: 'main', + schemaName: 'public', + tableName: 'users', + title: 'main/users', + columns: { + id: 'int4', + email: 'text' + } + } + ] + + const message = prepareAppUserMessage('Wire these together', undefined, additionalContext) + + const content = message.content as string + expect(content).toContain('- Frontend file: /index.tsx') + expect(content).toContain('- Backend runnable: loadUsers') + expect(content).not.toContain('fullFrontendContent') + expect(content).not.toContain('export async function main') + expect(content).not.toContain('Static inputs') + expect(content).not.toContain('Load users') + expect(content).toContain('**Table: main/users**') + expect(content).toContain('"id": "int4"') + expect(content).toContain('"email": "text"') + }) +}) diff --git a/frontend/src/lib/components/copilot/chat/app/core.ts b/frontend/src/lib/components/copilot/chat/app/core.ts index 0f421d04a1..95586df7a0 100644 --- a/frontend/src/lib/components/copilot/chat/app/core.ts +++ b/frontend/src/lib/components/copilot/chat/app/core.ts @@ -14,8 +14,6 @@ import { getDatatableSdkReference } from '$system_prompts' import { aiChatManager } from '../AIChatManager.svelte' import type { ContextElement, - AppFrontendFileElement, - AppBackendRunnableElement, AppCodeSelectionElement, AppDatatableElement } from '../context' @@ -82,28 +80,12 @@ export interface InspectorElementInfo { styles: Record } -/** Context about the currently selected file or runnable in the app editor */ +/** App editor context that is implicitly attached to app-mode AI messages. */ export interface SelectedContext { - /** Type of selection: 'frontend' for frontend files, 'backend' for backend runnables, or 'none' if nothing is selected */ - type: 'frontend' | 'backend' | 'none' - /** The path of the selected frontend file (when type is 'frontend') */ - frontendPath?: string - /** The content of the selected frontend file */ - frontendContent?: string - /** The key of the selected backend runnable (when type is 'backend') */ - backendKey?: string - /** The configuration of the selected backend runnable */ - backendRunnable?: BackendRunnable /** Inspector-selected element info (when user has used the inspector tool) */ inspectorElement?: InspectorElementInfo - /** Whether the file/runnable selection is excluded from being sent to the AI prompt */ - selectionExcluded?: boolean - /** Function to toggle whether the selection is excluded from the prompt */ - toggleSelectionExcluded?: () => void /** Function to clear the inspector selection */ clearInspector?: () => void - /** Function to clear the runnable selection (go back to frontend view) */ - clearRunnable?: () => void /** Code selection from the editor (either frontend or backend) */ codeSelection?: AppCodeSelectionElement /** Function to clear the code selection */ @@ -436,17 +418,6 @@ const getExecDatatableSqlToolDef = memo(() => ) ) -// ============= Selected Context Tool ============= - -const getGetSelectedContextSchema = memo(() => z.object({})) -const getGetSelectedContextToolDef = memo(() => - createToolDef( - getGetSelectedContextSchema(), - 'get_selected_context', - 'Get information about what is currently selected in the app editor. Returns the type of selection (frontend file or backend runnable) and the path/key of the selected item.' - ) -) - // ============= Lint Result Formatting ============= function formatLintMessages(messages: Record): string { @@ -560,22 +531,6 @@ export const getAppTools = memo((): Tool[] => [ return result } }, - // Selected context tool - { - def: getGetSelectedContextToolDef(), - fn: async ({ helpers, toolId, toolCallbacks }) => { - toolCallbacks.setToolStatus(toolId, { content: 'Getting selected context...' }) - const context = helpers.getSelectedContext() - const statusMsg = - context.type === 'frontend' - ? `Frontend file selected: ${context.frontendPath}` - : context.type === 'backend' - ? `Backend runnable selected: ${context.backendKey}` - : 'No selection' - toolCallbacks.setToolStatus(toolId, { content: statusMsg }) - return JSON.stringify(context, null, 2) - } - }, // Frontend tools { def: getGetFrontendFileToolDef(), @@ -1114,6 +1069,7 @@ When you are using the windmill-client, do not forget that as id for variables o 4. Use \`lint()\` at the end to check for and fix any remaining errors When creating a new app, use \`search_workspace\` or \`search_hub_scripts\` to find existing scripts/flows to reuse. +When the user mentions frontend files or backend runnables in context, only their identifiers are included. Use \`get_frontend_file\` or \`get_backend_runnable\` to inspect their contents before editing them or relying on implementation details. ` @@ -1158,60 +1114,12 @@ export function prepareAppUserMessage( // Check if we have any context to add const hasSelectedContext = - selectedContext && (selectedContext.type !== 'none' || selectedContext.inspectorElement) + selectedContext && (selectedContext.inspectorElement || selectedContext.codeSelection) const hasAdditionalContext = additionalContext && additionalContext.length > 0 if (hasSelectedContext || hasAdditionalContext) { content += `## SELECTED CONTEXT:\n` - // Add frontend file context with content (unless excluded) - if ( - selectedContext && - selectedContext.type === 'frontend' && - selectedContext.frontendPath && - !selectedContext.selectionExcluded - ) { - content += `The user is currently viewing the frontend file: **${selectedContext.frontendPath}**\n` - if (selectedContext.frontendContent) { - const truncatedContent = - selectedContext.frontendContent.length > MAX_CONTEXT_CONTENT_LENGTH - ? selectedContext.frontendContent.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + - '\n... [TRUNCATED]' - : selectedContext.frontendContent - content += `\n\`\`\`\n${truncatedContent}\n\`\`\`\n` - } - } - - // Add backend runnable context with content (unless excluded) - if ( - selectedContext && - selectedContext.type === 'backend' && - selectedContext.backendKey && - !selectedContext.selectionExcluded - ) { - content += `The user is currently viewing the backend runnable: **${selectedContext.backendKey}**\n` - if (selectedContext.backendRunnable) { - const runnable = selectedContext.backendRunnable - content += `- **Name**: ${runnable.name}\n` - content += `- **Type**: ${runnable.type}\n` - if (runnable.path) { - content += `- **Path**: ${runnable.path}\n` - } - if (runnable.inlineScript) { - const truncatedCode = - runnable.inlineScript.content.length > MAX_CONTEXT_CONTENT_LENGTH - ? runnable.inlineScript.content.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + - '\n... [TRUNCATED]' - : runnable.inlineScript.content - content += `- **Language**: ${runnable.inlineScript.language}\n` - content += `- **Code**:\n\`\`\`${runnable.inlineScript.language === 'bun' ? 'typescript' : 'python'}\n${truncatedCode}\n\`\`\`\n` - } - if (runnable.staticInputs && Object.keys(runnable.staticInputs).length > 0) { - content += `- **Static inputs**: ${JSON.stringify(runnable.staticInputs)}\n` - } - } - } - // Add inspector element context if available if (selectedContext?.inspectorElement) { const el = selectedContext.inspectorElement @@ -1249,34 +1157,9 @@ export function prepareAppUserMessage( for (const ctx of additionalContext) { if (ctx.type === 'app_frontend_file') { - const fileCtx = ctx as AppFrontendFileElement - content += `\n**Frontend File: ${fileCtx.path}**\n` - const truncatedContent = - fileCtx.content.length > MAX_CONTEXT_CONTENT_LENGTH - ? fileCtx.content.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + '\n... [TRUNCATED]' - : fileCtx.content - content += `\`\`\`\n${truncatedContent}\n\`\`\`\n` + content += `\n- Frontend file: ${ctx.path}\n` } else if (ctx.type === 'app_backend_runnable') { - const runnableCtx = ctx as AppBackendRunnableElement - const runnable = runnableCtx.runnable - content += `\n**Backend Runnable: ${runnableCtx.key}**\n` - content += `- **Name**: ${runnable.name}\n` - content += `- **Type**: ${runnable.type}\n` - if (runnable.path) { - content += `- **Path**: ${runnable.path}\n` - } - if (runnable.inlineScript) { - const truncatedCode = - runnable.inlineScript.content.length > MAX_CONTEXT_CONTENT_LENGTH - ? runnable.inlineScript.content.slice(0, MAX_CONTEXT_CONTENT_LENGTH) + - '\n... [TRUNCATED]' - : runnable.inlineScript.content - content += `- **Language**: ${runnable.inlineScript.language}\n` - content += `- **Code**:\n\`\`\`${runnable.inlineScript.language === 'bun' ? 'typescript' : 'python'}\n${truncatedCode}\n\`\`\`\n` - } - if (runnable.staticInputs && Object.keys(runnable.staticInputs).length > 0) { - content += `- **Static inputs**: ${JSON.stringify(runnable.staticInputs)}\n` - } + content += `\n- Backend runnable: ${ctx.key}\n` } else if (ctx.type === 'app_datatable') { const datatableCtx = ctx as AppDatatableElement const tableRef = diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 24860fb0a6..2ccd238eec 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -416,38 +416,14 @@ } }, getSelectedContext: () => { - const baseContext = { + return { inspectorElement: inspectorElement, - selectionExcluded: selectionExcludedFromPrompt, - toggleSelectionExcluded: toggleSelectionExcluded, clearInspector: clearInspectorSelection, - clearRunnable: handleClearRunnable, codeSelection: codeSelection, clearCodeSelection: () => { codeSelection = undefined } } - if (selectedRunnable) { - const runnable = convertToBackendRunnable(selectedRunnable, runnables[selectedRunnable]) - return { - type: 'backend' as const, - backendKey: selectedRunnable, - backendRunnable: runnable, - ...baseContext - } - } - if (selectedDocument) { - return { - type: 'frontend' as const, - frontendPath: selectedDocument, - frontendContent: files?.[selectedDocument], - ...baseContext - } - } - return { - type: 'none' as const, - ...baseContext - } }, snapshot: () => { // Force create snapshot for AI - it needs a restore point @@ -608,13 +584,8 @@ let selectedRunnable: string | undefined = $state(undefined) let selectedDocument: string | undefined = $state(undefined) let inspectorElement: InspectorElementInfo | undefined = $state(undefined) - let selectionExcludedFromPrompt: boolean = $state(false) let codeSelection: AppCodeSelectionElement | undefined = $state(undefined) - function toggleSelectionExcluded() { - selectionExcludedFromPrompt = !selectionExcludedFromPrompt - } - let modules = $state({}) as Modules // Normalize Windows-style path separators to Linux-style @@ -722,23 +693,17 @@ ) } - function handleClearRunnable() { - selectedRunnable = undefined - } - // Track previous values for change detection let prevSelectedRunnable: string | undefined = undefined let prevSelectedDocument: string | undefined = undefined - // Clear inspector and reset exclusion when selection changes + // Clear inspector when selection changes $effect(() => { if (selectedRunnable !== prevSelectedRunnable || selectedDocument !== prevSelectedDocument) { // Only clear if we're actually switching to something different if (prevSelectedRunnable !== undefined || prevSelectedDocument !== undefined) { clearInspectorSelection() } - // Reset exclusion when switching files/runnables - selectionExcludedFromPrompt = false prevSelectedRunnable = selectedRunnable prevSelectedDocument = selectedDocument } From 7fa924e67e212458726a839dbe366798b2709cd6 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:29:29 +0200 Subject: [PATCH 19/28] fix: correct flow conversation pagination (#8919) * fix: remove conversation after_id filter * fix: implement message after_id cursor * fix: use persisted cursor for chat polling Co-Authored-By: Claude Opus 4.5 * refactor: simplify message cursor ordering Co-Authored-By: Claude Opus 4.5 * refactor: use cte for message cursor Co-Authored-By: Claude Opus 4.5 * Update SQLx metadata * fix: use monotonic flow message cursor * Update SQLx metadata * fix: tighten flow message cursor pagination Co-Authored-By: Claude Opus 4.5 * Update SQLx metadata --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: windmill-internal-app[bot] --- ...5bdefedf8a51c4e6ddf0eca367b9cc778d051.json | 83 +++++++++++++++++++ ...153c43903f929ae5d62fbba12610f89c36d55.json | 2 +- ...4585e850f61a787a5b8f4a8d88f88016f1f2b.json | 22 ----- ...ca029380139f89a01a89033e36a6ed9ac082.json} | 12 ++- ..._conversation_message_created_seq.down.sql | 10 +++ ...ow_conversation_message_created_seq.up.sql | 10 +++ backend/summarized_schema.txt | 2 +- .../src/lib.rs | 73 +++++++++------- backend/windmill-api/openapi.yaml | 14 ++-- .../conversations/FlowChatManager.svelte.ts | 26 ++++-- 10 files changed, 187 insertions(+), 67 deletions(-) create mode 100644 backend/.sqlx/query-1c3473a0f9f6b6148b2c975f9f05bdefedf8a51c4e6ddf0eca367b9cc778d051.json delete mode 100644 backend/.sqlx/query-c67e81985093ff976f1326ff2254585e850f61a787a5b8f4a8d88f88016f1f2b.json rename backend/.sqlx/{query-a739af2f72e117acc58374f6ed44f8223efa4826d6c10639f98e624474b247a3.json => query-e8802be9203c1e88a06e337260ccca029380139f89a01a89033e36a6ed9ac082.json} (69%) create mode 100644 backend/migrations/20260423132025_flow_conversation_message_created_seq.down.sql create mode 100644 backend/migrations/20260423132025_flow_conversation_message_created_seq.up.sql diff --git a/backend/.sqlx/query-1c3473a0f9f6b6148b2c975f9f05bdefedf8a51c4e6ddf0eca367b9cc778d051.json b/backend/.sqlx/query-1c3473a0f9f6b6148b2c975f9f05bdefedf8a51c4e6ddf0eca367b9cc778d051.json new file mode 100644 index 0000000000..fb27bd9446 --- /dev/null +++ b/backend/.sqlx/query-1c3473a0f9f6b6148b2c975f9f05bdefedf8a51c4e6ddf0eca367b9cc778d051.json @@ -0,0 +1,83 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at, created_seq, step_name, success\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at, created_seq, step_name, success\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_seq DESC\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_seq ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "conversation_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "message_type: MessageType", + "type_info": { + "Custom": { + "name": "message_type", + "kind": { + "Enum": [ + "user", + "assistant", + "tool" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "content", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "job_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "created_seq", + "type_info": "Int8" + }, + { + "ordinal": 7, + "name": "step_name", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "success", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + false, + true, + false + ] + }, + "hash": "1c3473a0f9f6b6148b2c975f9f05bdefedf8a51c4e6ddf0eca367b9cc778d051" +} diff --git a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json index 36ddb8ab9f..713ccb9dd3 100644 --- a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json +++ b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - true + null ] }, "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" diff --git a/backend/.sqlx/query-c67e81985093ff976f1326ff2254585e850f61a787a5b8f4a8d88f88016f1f2b.json b/backend/.sqlx/query-c67e81985093ff976f1326ff2254585e850f61a787a5b8f4a8d88f88016f1f2b.json deleted file mode 100644 index 04e253ce54..0000000000 --- a/backend/.sqlx/query-c67e81985093ff976f1326ff2254585e850f61a787a5b8f4a8d88f88016f1f2b.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT created_at FROM flow_conversation_message WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "c67e81985093ff976f1326ff2254585e850f61a787a5b8f4a8d88f88016f1f2b" -} diff --git a/backend/.sqlx/query-a739af2f72e117acc58374f6ed44f8223efa4826d6c10639f98e624474b247a3.json b/backend/.sqlx/query-e8802be9203c1e88a06e337260ccca029380139f89a01a89033e36a6ed9ac082.json similarity index 69% rename from backend/.sqlx/query-a739af2f72e117acc58374f6ed44f8223efa4826d6c10639f98e624474b247a3.json rename to backend/.sqlx/query-e8802be9203c1e88a06e337260ccca029380139f89a01a89033e36a6ed9ac082.json index 8a64d7c0a6..a3374d6cdf 100644 --- a/backend/.sqlx/query-a739af2f72e117acc58374f6ed44f8223efa4826d6c10639f98e624474b247a3.json +++ b/backend/.sqlx/query-e8802be9203c1e88a06e337260ccca029380139f89a01a89033e36a6ed9ac082.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at, step_name, success\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at, step_name, success\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n ", + "query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at, created_seq, step_name, success\n FROM flow_conversation_message\n WHERE conversation_id = $1\n AND created_seq > $2\n ORDER BY created_seq ASC\n LIMIT $3\n ", "describe": { "columns": [ { @@ -46,11 +46,16 @@ }, { "ordinal": 6, + "name": "created_seq", + "type_info": "Int8" + }, + { + "ordinal": 7, "name": "step_name", "type_info": "Varchar" }, { - "ordinal": 7, + "ordinal": 8, "name": "success", "type_info": "Bool" } @@ -69,9 +74,10 @@ false, true, false, + false, true, false ] }, - "hash": "a739af2f72e117acc58374f6ed44f8223efa4826d6c10639f98e624474b247a3" + "hash": "e8802be9203c1e88a06e337260ccca029380139f89a01a89033e36a6ed9ac082" } diff --git a/backend/migrations/20260423132025_flow_conversation_message_created_seq.down.sql b/backend/migrations/20260423132025_flow_conversation_message_created_seq.down.sql new file mode 100644 index 0000000000..9ace9fae5f --- /dev/null +++ b/backend/migrations/20260423132025_flow_conversation_message_created_seq.down.sql @@ -0,0 +1,10 @@ +DROP INDEX IF EXISTS idx_conversation_message_conversation_created_seq; + +CREATE INDEX IF NOT EXISTS idx_conversation_message_conversation_time + ON flow_conversation_message(conversation_id, created_at DESC); + +ALTER TABLE flow_conversation_message + DROP CONSTRAINT IF EXISTS flow_conversation_message_created_seq_key; + +ALTER TABLE flow_conversation_message + DROP COLUMN IF EXISTS created_seq; diff --git a/backend/migrations/20260423132025_flow_conversation_message_created_seq.up.sql b/backend/migrations/20260423132025_flow_conversation_message_created_seq.up.sql new file mode 100644 index 0000000000..ef4d02ee07 --- /dev/null +++ b/backend/migrations/20260423132025_flow_conversation_message_created_seq.up.sql @@ -0,0 +1,10 @@ +ALTER TABLE flow_conversation_message + ADD COLUMN created_seq BIGINT GENERATED ALWAYS AS IDENTITY; + +ALTER TABLE flow_conversation_message + ADD CONSTRAINT flow_conversation_message_created_seq_key UNIQUE (created_seq); + +CREATE INDEX idx_conversation_message_conversation_created_seq + ON flow_conversation_message(conversation_id, created_seq); + +DROP INDEX IF EXISTS idx_conversation_message_conversation_time; diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index 1517a0e780..6553abb5cc 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -80,7 +80,7 @@ flow: workspace_id(char), path(char), summary(text), description(text), value(js FK: (workspace_id) -> workspace(id) flow_conversation: id(uuid), workspace_id(char), flow_path(char), title(char), created_at(ts), updated_at(ts), created_by(char) FK: (workspace_id) -> workspace(id) -flow_conversation_message: id(uuid), conversation_id(uuid), message_type(message_type), content(text), job_id(uuid), created_at(ts), step_name(char), success(bool) +flow_conversation_message: id(uuid), conversation_id(uuid), message_type(message_type), content(text), job_id(uuid), created_at(ts), created_seq(int8), step_name(char), success(bool) FK: (conversation_id) -> flow_conversation(id) | (job_id) -> v2_job(id) flow_iterator_data: job_id(uuid), itered(jsonb) flow_node: id(bigint), workspace_id(char), hash(bigint), path(char), lock(text), code(text), flow(jsonb), hash_v2(char(64)) diff --git a/backend/windmill-api-flow-conversations/src/lib.rs b/backend/windmill-api-flow-conversations/src/lib.rs index 70c96d1405..e85af5b83b 100644 --- a/backend/windmill-api-flow-conversations/src/lib.rs +++ b/backend/windmill-api-flow-conversations/src/lib.rs @@ -33,6 +33,7 @@ pub struct FlowConversationMessage { pub content: String, pub job_id: Option, pub created_at: DateTime, + pub created_seq: i64, pub step_name: Option, pub success: bool, } @@ -40,7 +41,11 @@ pub struct FlowConversationMessage { #[derive(Deserialize)] pub struct ListConversationsQuery { pub flow_path: Option, - pub after_id: Option, +} + +#[derive(Deserialize)] +pub struct ListMessagesQuery { + pub after_seq: Option, } async fn list_conversations( @@ -68,15 +73,6 @@ async fn list_conversations( if let Some(flow_path) = &query.flow_path { sqlb.and_where_eq("flow_path", "?".bind(flow_path)); } - if let Some(after_id) = &query.after_id { - let message_id_created_at = sqlx::query_scalar!( - "SELECT created_at FROM flow_conversation_message WHERE id = $1", - after_id - ) - .fetch_one(&mut *tx) - .await?; - sqlb.and_where_gt("created_at", "?".bind(&message_id_created_at.to_rfc3339())); - } sqlb.order_by("updated_at", true) .limit(per_page as i64) @@ -157,6 +153,7 @@ async fn list_messages( Extension(user_db): Extension, Path((w_id, conversation_id)): Path<(String, Uuid)>, Query(pagination): Query, + Query(query): Query, ) -> JsonResult> { let (per_page, offset) = paginate(pagination); let mut tx = user_db.clone().begin(&authed).await?; @@ -178,25 +175,43 @@ async fn list_messages( ))); } - // Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend - let messages = sqlx::query_as!( - FlowConversationMessage, - r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at, step_name, success - FROM ( - SELECT id, conversation_id, message_type, content, job_id, created_at, step_name, success - FROM flow_conversation_message - WHERE conversation_id = $1 - ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END - LIMIT $2 OFFSET $3 - ) AS messages - ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END - "#, - conversation_id, - per_page as i64, - offset as i64 - ) - .fetch_all(&mut *tx) - .await?; + let messages = if let Some(after_seq) = query.after_seq { + sqlx::query_as!( + FlowConversationMessage, + r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at, created_seq, step_name, success + FROM flow_conversation_message + WHERE conversation_id = $1 + AND created_seq > $2 + ORDER BY created_seq ASC + LIMIT $3 + "#, + conversation_id, + after_seq, + per_page as i64 + ) + .fetch_all(&mut *tx) + .await? + } else { + // Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend + sqlx::query_as!( + FlowConversationMessage, + r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at, created_seq, step_name, success + FROM ( + SELECT id, conversation_id, message_type, content, job_id, created_at, created_seq, step_name, success + FROM flow_conversation_message + WHERE conversation_id = $1 + ORDER BY created_seq DESC + LIMIT $2 OFFSET $3 + ) AS messages + ORDER BY created_seq ASC + "#, + conversation_id, + per_page as i64, + offset as i64 + ) + .fetch_all(&mut *tx) + .await? + }; tx.commit().await?; Ok(Json(messages)) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 17127ef3d6..8d04a06771 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -9454,13 +9454,13 @@ paths: schema: type: string format: uuid - - name: after_id - description: id to fetch only the messages after that id + - name: after_seq + description: Message sequence cursor to fetch only the messages after that cursor in: query required: false schema: - type: string - format: uuid + type: integer + format: int64 responses: "200": description: conversation messages @@ -20349,7 +20349,7 @@ components: FlowConversationMessage: type: object - required: [id, conversation_id, message_type, content, created_at] + required: [id, conversation_id, message_type, content, created_at, created_seq] properties: id: type: string @@ -20375,6 +20375,10 @@ components: type: string format: date-time description: When the message was created + created_seq: + type: integer + format: int64 + description: Monotonic cursor assigned when the message is inserted step_name: type: string description: The step name that produced that message diff --git a/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts b/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts index 66e5b09bc7..5cfe7ab3e3 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts +++ b/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts @@ -312,6 +312,17 @@ export class FlowChatManager { } } + private getLastPersistedMessageSeq() { + for (let i = this.messages.length - 1; i >= 0; i--) { + const message = this.messages[i] + if (!message.id.startsWith('temp-')) { + return message.created_seq + } + } + + return undefined + } + // Polling private async pollJobResult(jobId: string) { try { @@ -338,13 +349,13 @@ export class FlowChatManager { if (!get(workspaceStore)) return try { - const lastId = this.messages[this.messages.length - 1].id + const lastSeq = this.getLastPersistedMessageSeq() const response = await FlowConversationsService.listConversationMessages({ workspace: get(workspaceStore)!, conversationId: conversationId, page: 1, perPage: 50, - afterId: lastId + afterSeq: lastSeq }) if (options?.isNewConversation) { @@ -352,8 +363,6 @@ export class FlowChatManager { } const filteredResponse = response.filter((msg) => msg.message_type !== 'user') - - // Add any new intermediate messages not already present for (const msg of filteredResponse) { if (!this.messages.find((m) => m.id === msg.id)) { this.messages = [...this.messages, msg] @@ -363,7 +372,9 @@ export class FlowChatManager { // Only remove temporary messages when explicitly requested (e.g., after job completion) // During streaming, we keep temp messages to avoid them disappearing due to race conditions if (options?.removeTempMessages) { - this.messages = this.messages.filter((msg) => !msg.id.startsWith('temp-')) + this.messages = this.messages.filter( + (msg) => !msg.id.startsWith('temp-') || msg.message_type === 'user' + ) } } catch (error) { console.error('Polling error:', error) @@ -415,9 +426,10 @@ export class FlowChatManager { delete this.#conversationsCache[currentConversationId] const userMessage: ChatMessage = { - id: randomUUID(), + id: `temp-${randomUUID()}`, content: this.inputMessage.trim(), created_at: new Date().toISOString(), + created_seq: 0, message_type: 'user', conversation_id: currentConversationId } @@ -570,6 +582,7 @@ export class FlowChatManager { id: 'temp-' + randomUUID(), content: newContent, created_at: new Date().toISOString(), + created_seq: 0, message_type: 'tool', conversation_id: currentConversationId, job_id: '', @@ -596,6 +609,7 @@ export class FlowChatManager { id: assistantMessageId, content: accumulatedContent, created_at: new Date().toISOString(), + created_seq: 0, message_type: 'assistant', conversation_id: currentConversationId, job_id: '', From d6c642b170b9547fe1d8db190affa35b305c9c8a Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 23 Apr 2026 18:30:18 +0200 Subject: [PATCH 20/28] feat: add Azure Event Grid triggers (#8888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Azure Event Grid triggers (EE) Introduces a new enterprise trigger kind `azure` that supports three modes via a single unified trigger type: - basic_push: Azure Event Grid basic — custom topics, system topics (Storage, Resource Manager, Key Vault, etc.), domains (push only) - namespace_push: Event Grid Namespace topics (CloudEvents over HTTP push) - namespace_pull: Event Grid Namespace topics (HTTP pull with lock-token ack/reject for dead-lettering) Auth uses a Service Principal resource (tenant_id, client_id, client_secret, subscription_id). Subscriptions are created in CloudEvents 1.0 schema so the push webhook handler and the pull listener share one payload parser. Backend - New crate `windmill-trigger-azure` (OSS stubs + EE impl symlinked from windmill-ee-private) - Migration `azure_trigger` table with CHECK constraints enforcing mode/columns coherence - `TriggerKind::Azure`, `JobTriggerKind::Azure`, `DeployedObject::AzureTrigger` variants - Push route `/api/azure/w/{workspace}/*path` handles classic Event Grid SubscriptionValidation handshake and CloudEvents 1.0 abuse-protection OPTIONS handshake - Optional inbound JWT validation (audience check only for v1) - Feature flag `azure_trigger` propagated through windmill-api, windmill-store (resource helper), and added to ee_core Frontend - `triggers/azure/` editor with mode toggle (basic/namespace-push/ namespace-pull) and per-mode config (topic ARM id / namespace + topic name / subscription / filters / push auth / pull options) - Registered in icon map, display names, save functions, badge, wrapper, editor, add-trigger menu OpenAPI - `AzureTrigger`, `AzureTriggerData`, `AzureMode`, `AzureSubscriptionMode`, `AzureDeliveryConfig`, `TestAzureConnection` schemas; `/azure_triggers/*` endpoints; client regenerated Co-Authored-By: Claude Opus 4.7 (1M context) * chore: update ee-repo-ref to eaa7c3a9cb37a9ccc93f10a2535d929365acd2d8 This commit updates the EE repository reference after PR #541 was merged in windmill-ee-private. Previous ee-repo-ref: 9689014e8c12c36c1059fd8fa5758d550b8b8bc9 New ee-repo-ref: eaa7c3a9cb37a9ccc93f10a2535d929365acd2d8 Automated by sync-ee-ref workflow. * feat(azure-trigger): secret-auth push, ARM discovery, capture isolation, CLI + parity Frontend: - Split mode selector into Namespace/Basic + Pull/Push - ARM resource dropdowns (namespaces, Basic topics, namespace topics) populated from the service principal; cascade with stale-selection reset on SP / edition change - Remove stale authenticate toggle + audience input (server-managed push_auth_config has replaced them) - Azure listing page: "Create from template" button; "Also delete Azure subscription" toggle in the delete modal; simplified trigger label falling back to path - AzureCapture.svelte: "Test subscription name" with -wm-capture suffix - CompareWorkspaces.svelte: wire Azure for fork/compare - Drop Trigger-deployed/event-loss warning (capture subscription is isolated with -wm-capture) Backend: - Shared-secret push auth (see EE crate for detail) - JSONB push_auth_config column (renamed from delivery_config), #[serde(skip)] so clients/CLI/exports never see it - Drop redundant enabled column; mode supersedes - Azure capture infra: AzureTriggerConfig + set_azure_trigger_config + azure_payload route + TriggerKind::Azure arm; PT15M queue TTL on capture subscriptions so they bound storage after tab close - Granular ACLs, users offboarding, trash, git-sync deployed-object: all include azure_trigger CLI: - Add azure to TRIGGER_TYPES, pushObj dispatch, getTypeStrFromPath, trigger commands (get/update/create/list/template), sync delete switch + regex; e2e test for `trigger new --kind azure` - system_prompts: SCHEMA_MAPPINGS + schema_names include AzureTrigger; auto-generated/* regenerated Skill: - .claude/skills/adding-a-trigger/ checklist covering every file that needs editing when wiring a new trigger type (learned from this PR) ee-repo-ref bumped to b0e490cbf3724b7b64c6a5b010e3bdf24acd873c. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(azure-trigger): ci — ShareModal Kind + regenerated system_prompts - frontend/src/lib/components/ShareModal.svelte: add 'azure_trigger' to the Kind type so the listing page's "Permissions" action compiles (ts2345 — caught by npm_check on CI, missed by fast-check locally). - system_prompts/auto-generated/: regenerate to drop the stale delivery_config / AzureDeliveryConfig fields from the Azure schema (check-freshness on CI). Co-Authored-By: Claude Opus 4.7 (1M context) * refactor(azure-trigger): use workspace constant_time_eq crate Drop hand-rolled constant-time compare in favour of the workspace constant_time_eq crate (same one used by http_trigger_auth). ee-repo-ref bumped to 9659382d47286e7f7f66d01b6f5dd8d4ed34848b. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(azure-trigger): pass placeholder + disabled via inputProps `TextInput`'s `placeholder` and `disabled` go through its `inputProps` prop — CI's `npm run check` caught the stale top-level passing that `npm run check:fast` missed. Align with the DefaultEmailConfigSection pattern. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(azure-trigger): correct LATEST_GIT_SYNC_SCRIPT_PATH version to 28213 The hub deploy of the azure-aware sync-script is version 28213, not 28214. Backend was pinning a non-existent hub script, which broke the git_sync_e2e suite (every deploy's sync step 404'd). Co-Authored-By: Claude Opus 4.7 (1M context) * fix(azure-trigger): add azure_triggers to token scope selector + skill - windmill-api/src/token.rs: `build_trigger_scope_domains` was missing `("azure_triggers", "Azure Event Grid")`, so the CreateToken UI's scope selector didn't surface azure_triggers:read/write. Backend already had `ScopeDomain::AzureTriggers` wired (scopes.rs), this just exposes it. - .claude/skills/adding-a-trigger/SKILL.md: capture both scope-related files under the hardcoded-arrays section so future triggers don't miss the UI surface. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(adding-a-trigger-skill): clarify token.rs scope effect Not a regression — nothing was working before. Skipping TRIGGER_DOMAINS just means the scope works via API/CLI but has no UI checkbox. * docs(adding-a-trigger-skill): trim token.rs bullet * fix(azure-trigger): regen openapi-deref + swap textarea for TextInput - Run build_openapi.sh to regenerate openapi-deref.{yaml,json} with the 12 azure_triggers paths + schemas. These files are served by the runtime (include_str! in windmill-api/src/lib.rs) to external SDK consumers; without this regen the new endpoints wouldn't be advertised. - Replace the raw