How to Add a Workflow
Add state-machine orchestration to an existing PromptPack so that transitions between prompts happen automatically based on events, without the caller deciding which prompt to invoke.
Prerequisites
- A PromptPack with at least 2 prompts (v1.3+ schema; v1.4 adds the agent-loop fields shown in Step 5b below)
- Understanding of Pack Structure
- Familiarity with the prompts you want to orchestrate
Step 1: Identify Your States
Each state in the workflow maps to a prompt. Look at your existing prompts and decide:
- Which prompt receives initial input? → This becomes the entry state.
- What events cause transitions? → These become
on_eventkeys. - Which states are terminal? → On v1.4+ mark them with
terminal: true; on v1.3 leaveon_event: {}empty.
For example, if you have triage, billing, technical, and closing prompts:
flowchart LR triage([triage · entry]) -- billing --> billing_state[billing_state] triage -- technical --> tech_state[tech_state] billing_state -- resolved --> closing_state([closing_state]) tech_state -- resolved --> closing_stateStep 2: Define Entry and Events
Add the workflow top-level field with a version, entry, and states:
{ "workflow": { "version": 1, "entry": "triage", "states": { "triage": { "prompt_task": "triage", "description": "Classify and route incoming requests", "on_event": { "billing": "billing_state", "technical": "tech_state" } } } }}Key rules:
entrymust reference a key instatesprompt_taskmust reference a key inpromptson_eventvalues must reference other state keys
Step 3: Configure Persistence
Each state can be transient (default) or persistent:
transient: Conversation context resets when entering this state. Good for classification/routing states where prior context isn’t needed.persistent: Conversation context is preserved across turns. Good for states where the agent needs to remember what was discussed.
{ "billing_state": { "prompt_task": "billing", "on_event": { "resolved": "closing_state", "escalate": "escalation" }, "persistence": "persistent" }}Persistent states consume more tokens because the full conversation history is maintained. Use transient for stateless operations like classification or routing.
Step 4: Set Orchestration Mode
Each state can declare how it interacts with external systems:
internal(default): The runtime handles execution entirely within the packexternal: The state hands off to an external system (e.g., human-in-the-loop)hybrid: The runtime executes the prompt but may also coordinate with external systems
{ "escalation": { "prompt_task": "closing", "on_event": {}, "orchestration": "external" }}Step 5: Add Terminal States
In v1.4+, mark exit states explicitly with terminal: true:
{ "closing_state": { "prompt_task": "closing", "terminal": true, "orchestration": "internal" }}For backward compatibility with v1.3, an empty on_event: {} is still treated as terminal:
{ "closing_state": { "prompt_task": "closing", "on_event": {}, "orchestration": "internal" }}Prefer terminal: true on new packs — it’s explicit, easier to read, and lets static analyzers flag terminal states without inspecting on_event contents.
Step 5b: Bound Agent Loops (v1.4+)
If a state can be re-entered (e.g., implement → test → implement in a code-generation loop), bound it with max_visits and route the overflow to a fallback state with on_max_visits:
{ "implement": { "prompt_task": "implement", "max_visits": 5, "on_max_visits": "review", "on_event": { "CodeReady": "test" } }, "review": { "prompt_task": "review", "terminal": true }}For lightweight, structured state that needs to flow across visits (commit SHAs, test reports, iteration logs), declare artifacts:
{ "implement": { "prompt_task": "implement", "max_visits": 5, "on_max_visits": "review", "artifacts": { "commit_sha": { "type": "text/plain", "description": "Latest commit" }, "test_report": { "type": "application/json", "description": "Test runner output" }, "iteration_log": { "type": "text/plain", "mode": "append", "description": "Per-visit log" } }, "on_event": { "CodeReady": "test" } }}Templates read artifact values as {{artifacts.commit_sha}} etc. mode: "append" accumulates across visits; the default "replace" overwrites.
Add a workflow-level budget as a global runaway-loop safety net:
{ "workflow": { "version": 1, "entry": "plan", "states": { /* … */ }, "engine": { "budget": { "max_total_visits": 50, "max_tool_calls": 200, "max_wall_time_sec": 600 } } }}engine.budget is a backstop — set it on any pack that contains a loop, even if every state has its own max_visits. See Agent Loops and RFC-0009 for the full design.
Complete Example
Here’s a PromptPack before adding a workflow (caller must choose which prompt to use):
{ "id": "support-pack", "name": "Support Pack", "version": "1.0.0", "template_engine": { "version": "v1", "syntax": "{{variable}}" }, "prompts": { "triage": { "id": "triage", "name": "Triage", "version": "1.0.0", "system_template": "Classify the request as billing or technical.", "parameters": { "temperature": 0.3 } }, "billing": { "id": "billing", "name": "Billing", "version": "1.0.0", "system_template": "Handle billing inquiries.", "parameters": { "temperature": 0.5 } }, "technical": { "id": "technical", "name": "Technical", "version": "1.0.0", "system_template": "Provide technical troubleshooting.", "parameters": { "temperature": 0.4 } }, "closing": { "id": "closing", "name": "Closing", "version": "1.0.0", "system_template": "Summarize the resolution.", "parameters": { "temperature": 0.6 } } }}And after adding workflow orchestration:
{ "id": "support-pack", "name": "Support Pack", "version": "2.0.0", "template_engine": { "version": "v1", "syntax": "{{variable}}" }, "prompts": { "triage": { "id": "triage", "name": "Triage", "version": "1.0.0", "system_template": "Classify the request as billing or technical. Respond with exactly one word: billing or technical.", "parameters": { "temperature": 0.3 } }, "billing": { "id": "billing", "name": "Billing", "version": "1.0.0", "system_template": "Handle billing inquiries. When the issue is resolved, respond with: resolved. If you cannot resolve it, respond with: escalate.", "parameters": { "temperature": 0.5 } }, "technical": { "id": "technical", "name": "Technical", "version": "1.0.0", "system_template": "Provide technical troubleshooting. When the issue is resolved, respond with: resolved. If you cannot resolve it, respond with: escalate.", "parameters": { "temperature": 0.4 } }, "closing": { "id": "closing", "name": "Closing", "version": "1.0.0", "system_template": "Summarize the resolution and ask if there's anything else.", "parameters": { "temperature": 0.6 } } }, "workflow": { "version": 1, "entry": "triage", "states": { "triage": { "prompt_task": "triage", "description": "Classify and route incoming requests", "on_event": { "billing": "billing_state", "technical": "tech_state" } }, "billing_state": { "prompt_task": "billing", "on_event": { "resolved": "closing_state", "escalate": "escalation" }, "persistence": "persistent" }, "tech_state": { "prompt_task": "technical", "on_event": { "resolved": "closing_state", "escalate": "escalation" }, "persistence": "persistent" }, "closing_state": { "prompt_task": "closing", "on_event": {}, "orchestration": "internal" }, "escalation": { "prompt_task": "closing", "on_event": {}, "orchestration": "external" } } }}Validation Checklist
-
workflow.entryreferences a valid state key - Every
prompt_taskreferences a valid prompt key - Every
on_eventvalue references a valid state key - At least one state is terminal — either
terminal: true(v1.4+) or emptyon_event: {} - All states are reachable from the entry state
- On v1.4: any re-enterable state declares
max_visits, and the workflow has anengine.budget - On v1.4:
on_max_visits(if set) references a valid state key - On v1.4: every key in an
artifactsmap declares atype(MIME type) - Pack validates against the v1.4 JSON schema (or v1.3 if you’re not using agent-loop fields)
:::warning Common Mistakes
- Circular loops without exit: Make sure there’s always a path to a terminal state. A
billing → triage → billingcycle with noresolvedevent creates an infinite loop. - Missing prompt_task: Every state must have a
prompt_taskthat matches a key inprompts. A typo here will fail schema validation. - Prompt doesn’t emit expected events: The workflow routes based on events like
"billing"or"resolved", but these must actually appear in the prompt’s output. Update yoursystem_templateto instruct the model to emit the right event strings. :::
Next Steps
- How to Set Up Agents — combine workflow with A2A discovery
- Architecture Patterns — Router+Specialists, Pipeline, Agent Loop, and other patterns
- Code-Generation Loop example — full v1.4 agent-loop pack
- RFC 0005: Workflow Extension — workflow design rationale
- RFC 0009: Agent Loop Extension — agent-loop design rationale