Agent ↔ FSM-action handshake
This is the contract between an FSM action and the agent it invokes. Most "platform bug" misdiagnoses live in the gap between "how the FSM authors the action" and "what the agent prompt expects." Read this once, then ground every job/agent edit in it.
The four invocation kinds
| FSM action | Referenced agent | Runtime path |
|---|---|---|
type: llm inline |
none — the action carries provider, model, prompt, tools directly |
fsm-engine.ts case "llm" |
type: agent |
agents.<id> block with type: llm |
fsm-engine.ts case "agent" → runAgent → from-llm.ts |
type: agent |
agents.<id> with type: atlas |
case "agent" → runAgent → bundled-agent handler |
type: agent |
agents.<id> with type: user |
case "agent" → runAgent → ProcessAgentExecutor (NATS subprocess) |
Each path has the same outputTo contract but different tool
surfaces and prompt-composition rules.
What the runtime auto-injects
Regardless of invocation kind, the LLM-side agent (any agent that's
type: llm) sees:
- Platform tools — full
PLATFORM_TOOL_NAMESset frompackages/agent-sdk/src/platform-tools.ts(~35 entries spanning memory, artifacts, fs, shell/data, state, HITL, and flow-control likedelegate). These inject on top of the agent'sconfig.toolsallowlist regardless of what's intools:. See source for the exact set. completetool — injected when the action declaresoutputTo. The LLM MUST call this to surface output (see "Thecompletecontract" below).failSteptool — injected for inlinetype: llmFSM actions (seefsm-engine.ts case "llm"). The LLM calls this to deliberately fail the step with a structured reason. NOT injected on thetype: agent→type: llmpath that goes throughfrom-llm.ts; an agent reachable through both shapes can't assume the tool is present.
What's NOT auto-injected:
- MCP server tools — those come from
agents.<id>.config.tools(workspace-scope MCP only) or from the inlinetype: llmaction'stools:array.
The complete contract
The single most important rule: if an action has outputTo, the
agent's prompt MUST instruct the LLM to call complete with the
structured output.
When the action is:
- type: agent
agentId: stale-report-agent
outputTo: response
The runtime injects a complete tool with input schema {response: string} (or richer if outputType is declared). The agent's prompt
should end with:
When done, call the `complete` tool with:
{ "response": "<the full final text>" }
This is how the FSM captures your output. Do not stream the text as
your final assistant message — the runtime reads it from `complete`.
Without this instruction, the LLM streams prose, never calls
complete, and the runtime throws:
LLM action with outputTo 'response' did not call complete
outputType and structured output
When you set outputType: SomeDoc, the runtime looks up
fsm.documentTypes.SomeDoc for a JSON Schema and feeds it to the
complete tool's input schema. The LLM must produce data matching
the schema.
documentTypes:
EmailDraft:
type: object
properties:
to: { type: string }
subject: { type: string }
body: { type: string }
required: [to, subject, body]
states:
drafting:
entry:
- type: agent
agentId: drafter
outputTo: draft
outputType: EmailDraft
The agent's prompt should know about to, subject, body —
either from the prompt body or from a worked example.
inputFrom and document chaining
inputFrom reads a prior step's outputTo document and feeds it
into the next step's input. String form chains one prior; array
form concatenates multiple labeled.
states:
draft:
entry:
- type: agent
agentId: drafter
outputTo: draft-doc
- type: emit
event: DONE
on: { DONE: { target: review } }
review:
entry:
- type: agent
agentId: reviewer
inputFrom: draft-doc # reads from draft-doc
Bulky inputs are passed as artifactRefs plus a compact summary —
the agent reads the full data via parse_artifact(<id>) if needed.
Pattern A vs Pattern B
Two job shapes:
- Pattern A (
execution.strategy: sequential): list of agent IDs that run in order. Final agent's assistant text becomes the session output natively. Use when the work is a single LLM conversation, no document chaining needed, output is plain text. - Pattern B (
fsm:): state machine with named states, entry actions, and explicitoutputTodocuments. Required when you need branching, multi-step document flow, parallel actions, or HITL elicitation.
Don't migrate Pattern A → Pattern B "to get outputTo." The
migration introduces the complete injection contract, which is
the single most common source of empty-output failures. If a
Pattern A job is producing empty output, fix the agent's prompt
first.
Cross-references
@friday/writing-workspace-jobs— FSM authoring shapes.@friday/workspace-api— agent CRUD.@friday/delegate-handoff— sibling contract for the parent ↔ delegate-child boundary.@friday/debugging-empty-output— what happens when this contract is violated.@friday/debugging-runtime-errors—did not call completeand related errors.