Summary
An ai.exposed action whose target is a screen flow cannot be completed through MCP. run_action starts the flow, the screen node suspends the run and returns the form definition, and the MCP tool set has no way to resume it. The agent can press the button but never finish. Two independent causes, both in the platform:
service-automation screen executor pauses whenever the node has fields, without checking whether those fields are already bound as flow variables. runtime's seedFlowActionParams does merge the MCP params into the flow's variables, so correctly-named params bind — and the screen still pauses.
runtime's summarizeActionParams only reads action.params (script-action declarations). A flow's isInput variables are never surfaced, so list_actions returns no params for any flow-typed action even though the tool description promises "its input parameters". The agent has no way to learn the input names it would need for (1) to matter.
Environment
@objectstack/mcp, runtime, service-automation, spec — all 17.2.0
- App:
objectstack-ai/hotcrm @ 5e08628f, dev server on http://localhost:4001
- Client: Claude Code (desktop) →
http://localhost:4001/api/v1/mcp with x-api-key (principal: human, no onBehalfOf)
Reproduction
hotcrm's schedule_followup action (src/actions/lead.actions.ts) is type: 'flow', target: 'schedule_followup', ai.exposed: true. The target flow (src/flows/schedule-followup.flow.ts) is type: 'screen' and declares subject, dueDate, activityType, priority, notes as isInput: true variables; its first node after start is screen_1 with the same five fields.
list_actions →
{ "name": "schedule_followup", "objectName": "crm_lead", "type": "flow",
"requiresRecord": true, "requiresConfirmation": false }
No params key, so the caller cannot know the input names.
run_action({ actionName: "schedule_followup", objectName: "crm_lead", recordId: "<lead id>", params: { subject: "…", due_date: "2026-09-09" } }) →
{ "ok": true, "result": { "success": true, "status": "paused",
"runId": "run_a618315c-…", "durationMs": 2,
"screen": { "nodeId": "screen_1", "title": "Schedule Follow-up",
"fields": [ {"name":"subject",…}, {"name":"dueDate",…}, … ] } } }
- Verify: no
crm_task with related_to_lead = <lead id>; the lead's next_followup_date and updated_at are unchanged. The run is parked on screen_1 with nothing able to resume it — the 11 registered MCP tools (list_objects … run_action) include no resume / submit-screen verb.
Passing the correct names (dueDate instead of due_date) does not change the outcome: the variables bind, the screen node still suspends.
Where it happens
@objectstack/service-automation screen node execute():
shouldPause = cfg.waitForInput === true || (hasFields && cfg.waitForInput !== false) — the only inputs to the decision are "does the node have fields" and the author-level waitForInput flag. Whether every required field already has a bound variable is never consulted. waitForInput: false is not a fix: it also skips the form for interactive users.
@objectstack/runtime summarizeActionParams(deps, action, obj): iterates action.params only. For a flow-typed action the flow's variables.filter(v => v.isInput) (and/or the first screen node's fields) are the real input contract and are not consulted.
@objectstack/runtime seedFlowActionParams: works as intended — returns { ...record, recordId, <objectName>Id, ...params }, so this is not where the values are lost.
@objectstack/mcp registerActionTools: run_action just forwards to bridge.runAction and returns the result verbatim, so the paused envelope reaches the agent but there is nothing it can do with runId.
Expected
Either (or both):
- Headless satisfaction of a screen: when a run is started with
params and every required field of the screen node is already bound (and optional ones default), the executor should treat the screen as satisfied and continue instead of suspending. Interactive runs (no params supplied) keep the current behaviour.
- A resume verb on the MCP surface: e.g.
resume_run({ runId, values }) that posts screen values into a paused run — mirroring what the console's screen runner does — so an agent that receives status: "paused" + screen can complete it.
And independently:
list_actions should surface a flow action's inputs (isInput variables, with the screen field's label/type/required/options where available) as params, matching the tool description.
Why it matters
ai.exposed today means "the agent can invoke this", not "the agent can complete this". Every screen-typed flow action is a dead end for MCP clients, and the agent has no signal telling it so — list_actions looks identical for a script action that will run to completion and a screen flow that will park. In hotcrm both AI-exposed actions (schedule_followup, convert_lead) are screen flows, so the action layer is 0-for-2 over MCP while the generic CRUD tools work fully. The fallback an agent ends up taking (create_record + update_record to re-implement the flow's tail by hand) bypasses whatever business rules the flow encapsulated.
Related: #2849 established invoke-time as the only agent boundary for actions; this issue is about the other half — once invoked, the action must be completable.
Summary
An
ai.exposedaction whose target is a screen flow cannot be completed through MCP.run_actionstarts the flow, the screen node suspends the run and returns the form definition, and the MCP tool set has no way to resume it. The agent can press the button but never finish. Two independent causes, both in the platform:service-automationscreen executor pauses whenever the node has fields, without checking whether those fields are already bound as flow variables.runtime'sseedFlowActionParamsdoes merge the MCPparamsinto the flow's variables, so correctly-named params bind — and the screen still pauses.runtime'ssummarizeActionParamsonly readsaction.params(script-action declarations). A flow'sisInputvariables are never surfaced, solist_actionsreturns noparamsfor any flow-typed action even though the tool description promises "its input parameters". The agent has no way to learn the input names it would need for (1) to matter.Environment
@objectstack/mcp,runtime,service-automation,spec— all 17.2.0objectstack-ai/hotcrm@5e08628f, dev server onhttp://localhost:4001http://localhost:4001/api/v1/mcpwithx-api-key(principal: human, no onBehalfOf)Reproduction
hotcrm's
schedule_followupaction (src/actions/lead.actions.ts) istype: 'flow',target: 'schedule_followup',ai.exposed: true. The target flow (src/flows/schedule-followup.flow.ts) istype: 'screen'and declaressubject,dueDate,activityType,priority,notesasisInput: truevariables; its first node afterstartisscreen_1with the same five fields.list_actions→{ "name": "schedule_followup", "objectName": "crm_lead", "type": "flow", "requiresRecord": true, "requiresConfirmation": false }No
paramskey, so the caller cannot know the input names.run_action({ actionName: "schedule_followup", objectName: "crm_lead", recordId: "<lead id>", params: { subject: "…", due_date: "2026-09-09" } })→{ "ok": true, "result": { "success": true, "status": "paused", "runId": "run_a618315c-…", "durationMs": 2, "screen": { "nodeId": "screen_1", "title": "Schedule Follow-up", "fields": [ {"name":"subject",…}, {"name":"dueDate",…}, … ] } } }crm_taskwithrelated_to_lead = <lead id>; the lead'snext_followup_dateandupdated_atare unchanged. The run is parked onscreen_1with nothing able to resume it — the 11 registered MCP tools (list_objects … run_action) include no resume / submit-screen verb.Passing the correct names (
dueDateinstead ofdue_date) does not change the outcome: the variables bind, the screen node still suspends.Where it happens
@objectstack/service-automationscreen nodeexecute():shouldPause = cfg.waitForInput === true || (hasFields && cfg.waitForInput !== false)— the only inputs to the decision are "does the node have fields" and the author-levelwaitForInputflag. Whether everyrequiredfield already has a bound variable is never consulted.waitForInput: falseis not a fix: it also skips the form for interactive users.@objectstack/runtimesummarizeActionParams(deps, action, obj): iteratesaction.paramsonly. For a flow-typed action the flow'svariables.filter(v => v.isInput)(and/or the first screen node'sfields) are the real input contract and are not consulted.@objectstack/runtimeseedFlowActionParams: works as intended — returns{ ...record, recordId, <objectName>Id, ...params }, so this is not where the values are lost.@objectstack/mcpregisterActionTools:run_actionjust forwards tobridge.runActionand returns the result verbatim, so the paused envelope reaches the agent but there is nothing it can do withrunId.Expected
Either (or both):
paramsand everyrequiredfield of the screen node is already bound (and optional ones default), the executor should treat the screen as satisfied and continue instead of suspending. Interactive runs (no params supplied) keep the current behaviour.resume_run({ runId, values })that posts screen values into a paused run — mirroring what the console's screen runner does — so an agent that receivesstatus: "paused"+screencan complete it.And independently:
list_actionsshould surface a flow action's inputs (isInputvariables, with the screen field'slabel/type/required/optionswhere available) asparams, matching the tool description.Why it matters
ai.exposedtoday means "the agent can invoke this", not "the agent can complete this". Every screen-typed flow action is a dead end for MCP clients, and the agent has no signal telling it so —list_actionslooks identical for a script action that will run to completion and a screen flow that will park. In hotcrm both AI-exposed actions (schedule_followup,convert_lead) are screen flows, so the action layer is 0-for-2 over MCP while the generic CRUD tools work fully. The fallback an agent ends up taking (create_record+update_recordto re-implement the flow's tail by hand) bypasses whatever business rules the flow encapsulated.Related: #2849 established invoke-time as the only agent boundary for actions; this issue is about the other half — once invoked, the action must be completable.