This is the guide for writing Raymond workflow state files — the markdown prompts and shell scripts that define your workflows. It covers everything you need to know as a workflow author without getting into Raymond's internal implementation.
For the authoritative protocol specification, see workflow-protocol.md. For architecture and internals, see orchestration-design.md.
A workflow is a collection of state files that reference each other via transition tags. Raymond runs these files in sequence, following the transitions your prompts declare. The collection may be a plain directory or a zip archive.
A state file is either:
- A markdown prompt (
.md) — sent to Claude Code for LLM interpretation - A shell script (
.shon Unix,.bator.ps1on Windows) — executed directly, no LLM involved
An agent is a logical thread of execution within a workflow. Workflows
start with one agent (main) and can spawn more via <fork>.
Workflow scoping: All state files in a workflow must live within the same
scope — either the same directory or the same zip archive. Transitions reference
filenames only — no paths, no / or \ characters. The workflow's scope is
the directory containing all state files, or the zip archive when one is used
as input.
Raymond runs markdown states against an agent backend — the CLI tool that actually invokes an LLM and executes tool calls. Two backends are supported:
- Claude (default) — runs against
claude(Claude Code). Uses Anthropic models via Claude's own account/auth. - pi — runs against
pi(the pi coding agent). Pi is multi-provider: one workflow can target Anthropic, OpenAI, Google, or other providers via pi's--provider/--modelselectors.
Shell-script states do not use any backend — they execute directly.
A workflow opts into the pi backend in its manifest. Without a backend:
declaration, the workflow uses Claude.
# workflow.yaml
backend: piFor pi-specific options, use the structured form:
backend:
name: pi
options:
provider: anthropic # --provider
thinking: medium # --thinking <off|low|medium|high|xhigh>
tools: [read, edit, write, grep, find, ls] # --tools allowlist
extensions: # --extension (repeatable)
- <npm-package-or-path>
skills: # --skill (repeatable)
- ./skills/<dir>All options fields are optional. When you omit tools, raymond passes a
conservative default allowlist (read, edit, write, grep, find, ls — pi's
built-in tools minus bash) so destructive shell commands require an
explicit opt-in. The --dangerously-skip-permissions CLI flag drops this
default and falls back to pi's full built-in surface.
Claude prompts the user before destructive tool calls (unless
--dangerously-skip-permissions is set). Pi has no per-call permission
prompts — its safety boundary is the static --tools allowlist. For pi
workflows that need a tight surface, declare backend.options.tools with
just what the workflow needs. To opt out of tools entirely, use
backend.options.no_tools: true.
A <call-workflow>, <function-workflow>, <fork-workflow>, or
<reset-workflow> may target a workflow that declares a different backend.
Raymond switches backends per nested workflow — a Claude workflow can call
a pi workflow and vice versa. Each workflow runs on whatever backend its
own manifest declares.
- No usage-limit detection. Provider rate limits surface as generic failures (Claude has special handling for "hit your limit" messages).
- No live per-event cost. The dashboard shows
—for in-flight cost on pi states; the per-turn cost lands after the turn completes. - No MCP server support. Pi exposes tools via its own
--extensionand--skillmechanisms. Workflows that depend on Claude's MCP integrations do not run under pi. For the full feature comparison and rationale, see pi-backend.md.
Markdown states are prompts interpreted by Claude Code. Write them as you would any Claude Code prompt, with one addition: instruct the agent to emit a transition tag to signal what happens next.
Review the code for issues. If everything looks good, emit:
<goto>COMMIT.md</goto>The agent's final message must contain exactly one transition tag. The tag can appear anywhere in the message.
The Claude Code instance executing a state does not know it is part of a workflow. It sees your prompt, nothing more. It has no awareness of other state files, the overall workflow structure, or what steps come later. This means it will try to be helpful by doing as much as it can — which often means racing ahead and doing work that belongs to later steps.
To prevent this, every state prompt should clearly define its scope:
- State what to do. Be specific about the task for this step.
- State when to stop. Use explicit language like "STOP after writing the
file" or "After reading the file, respond with exactly:
<goto>NEXT.md</goto>". - State what not to do yet. If the agent can infer what logically comes next (and it will), tell it not to do that yet. Use the word "yet" or phrases like "that happens in a later step" — this prevents the constraint from lingering in context and blocking later steps that should perform those actions.
Example — too vague:
Read the requirements file. Once done, proceed to the next step:
<goto>GENERATE.md</goto>The word "proceed" is ambiguous. The agent may interpret it as permission to
keep going and start doing the work that GENERATE.md is supposed to handle.
Example — explicit scope:
Read the requirements from requirements.md.
STOP after reading the file. Do not generate any code or create any files
yet — that happens in a later step.
After reading the file, respond with exactly:
<goto>GENERATE.md</goto>For states with implicit transitions (single allowed transition in frontmatter), the agent doesn't need to emit a tag, but scope boundaries are still important. The agent can still race ahead and do work belonging to later steps:
---
allowed_transitions:
- { tag: goto, target: REVIEW.md }
---
Create a plan in plan.md covering all the requirements.
STOP after writing plan.md. Do not start implementing yet — that happens
in a later step.Markdown states can optionally include YAML frontmatter to declare policy:
---
allowed_transitions:
- { tag: goto, target: REVIEW.md }
- { tag: goto, target: DONE.md }
- { tag: result }
---
Your prompt text here...Frontmatter enables two features:
1. Transition validation: The orchestrator enforces that the agent's output matches one of the declared transitions. If it doesn't, the agent gets re-prompted with a reminder listing the valid options (up to 3 retries).
2. Implicit transitions: When frontmatter declares exactly one allowed
transition, the agent doesn't need to emit the tag at all — the orchestrator
assumes it. This saves tokens when only one path is possible. (A bare
{ tag: result } without a payload key cannot be implicit, since the
orchestrator doesn't know what payload to use.)
---
allowed_transitions:
- { tag: goto, target: NEXT.md }
---
Do the work. You don't need to emit a transition tag.Fixed-payload results: Add a payload key to constrain <result> values.
When a single fixed-payload result is the only allowed transition, it becomes
implicit — the agent doesn't need to emit a tag.
---
allowed_transitions:
- { tag: result, payload: "YES" }
- { tag: result, payload: "NO" }
---
Is the task complete? Reply with <result>YES</result> or <result>NO</result>With a single fixed-payload result, the transition is implicit:
---
allowed_transitions:
- { tag: result, payload: "DONE" }
---
Process the data. No need to emit a transition tag.A { tag: result } entry without payload still allows any payload and
cannot be implicit.
3. Forced implicit dispatch (force_implicit: true): an implicit-eligible
state can opt out of transition parsing entirely. The agent still runs and
side effects (tool use, <print>) are preserved, but any transition-looking
tags in its output are ignored — the policy's transition fires
unconditionally.
---
allowed_transitions:
- { tag: goto, target: NEXT.md }
force_implicit: true
---
Explain how <goto> tags interact with the return stack. Tag examples in
your answer will not trigger a transition.Reach for this when (a) the prompt itself discusses transition-tag syntax as
subject matter, or (b) the model is "over-eager" and emits inferred tags
that disagree with the policy, causing spurious reminder retries. The
linter requires the policy to be implicit-eligible — multiple transitions,
<ask>, or bare <result> with force_implicit are flagged as errors.
Without frontmatter, all transitions are allowed but the orchestrator cannot recover from missing or invalid tags — failures are fatal.
Frontmatter can specify which model to use for a state:
---
model: haiku
allowed_transitions:
- { tag: result }
---
Is this task complete? Reply with <result>YES</result> or <result>NO</result>For Claude workflows, valid values are opus, sonnet, haiku.
For pi workflows, the value is passed through to pi's --model flag
verbatim; use a pi-recognized model id, optionally with a provider/id
prefix to disambiguate (e.g. anthropic/claude-sonnet-4-6,
openai/gpt-5). Pi's default provider/model is used when model: is
omitted.
This overrides the --model CLI flag. Precedence: frontmatter model >
CLI --model > backend default.
Use this to run cheap evaluators on haiku (or the equivalent for your pi
provider) while keeping complex reasoning on a stronger model.
Frontmatter can specify the effort level for extended thinking:
---
effort: high
allowed_transitions:
- { tag: goto, target: NEXT.md }
---
Analyze this complex problem carefully...Valid values: low, medium, high. This overrides the --effort CLI flag.
Precedence: frontmatter effort > CLI --effort > backend default.
For pi workflows, effort: maps to pi's --thinking flag. The common
values (low, medium, high) translate cleanly; pi also accepts off
and xhigh, which can be set via frontmatter and are passed through
verbatim.
Use high effort for complex reasoning tasks, medium for balanced performance,
or low for simple tasks where speed is preferred over depth.
Shell scripts execute directly without invoking an LLM. Use them whenever the work is deterministic and doesn't need reasoning: polling, builds, data processing, environment setup, health checks, cleanup.
#!/bin/bash
# POLL.sh - Check for new issues
response=$(curl -s "$GITHUB_API/issues?state=open")
count=$(echo "$response" | jq 'length')
if [ "$count" -gt 0 ]; then
echo "<goto>PROCESS.md</goto>"
else
sleep 60
echo "<reset>POLL.sh</reset>"
fiScripts emit the same transition tags to stdout. The orchestrator parses them identically to LLM output.
Key differences from markdown states:
| Aspect | Markdown | Scripts |
|---|---|---|
| Execution | LLM interprets prompt | Shell executes directly |
| Cost | Token cost per run | Zero |
| Error recovery | Can re-prompt on failures | Fatal on errors |
| Frontmatter policy | Supported | Not supported |
Scripts receive workflow context via environment variables:
| Variable | Description | Example |
|---|---|---|
RAYMOND_WORKFLOW_ID |
Workflow run identifier | wf-2026-01-15-abc123 |
RAYMOND_AGENT_ID |
Current agent identifier | main, main_worker1 |
RAYMOND_TASK_FOLDER |
Agent's task folder (see {{task_folder}}) |
.raymond/tasks/wf-.../main |
RAYMOND_INPUT |
Inbound payload — same channel as {{input}} for markdown states (see Template Variables) |
(unset when empty) |
RAYMOND_CONTEXT_TOKENS |
Context-window fullness of this agent's session, as of the end of the previous turn (same value as {{context_tokens}}); 0 on a fresh session |
45072 |
RAYMOND_SESSION_ID |
This agent's current backend session id (empty before the first turn); usable with ray --claude_context_for_session=$RAYMOND_SESSION_ID |
00038246-4d48-... |
RAYMOND_WF_PARENT |
Absolute path of the directory containing this workflow (same value as {{wf_parent}}); use it to reach shipped guidance docs, e.g. "$RAYMOND_WF_PARENT/docs/my_tool.md" |
/srv/workflows |
Scripts using <reset> to loop start fresh each time. To persist data across
iterations, write to files:
#!/bin/bash
counter_file="/tmp/poll_counter.txt"
if [ -f "$counter_file" ]; then
count=$(cat "$counter_file")
else
count=0
fi
count=$((count + 1))
echo $count > "$counter_file"
if [ $count -lt 5 ]; then
echo "<reset>POLL.sh</reset>"
else
rm -f "$counter_file"
echo "<result>Polling complete after $count iterations</result>"
fiScripts can emit <print> tags to surface status messages before the state
terminates. Each tag fires a PrintOutput event that the console observer
writes to the terminal in real time:
#!/bin/bash
echo "<print>Cloning repository…</print>"
git clone "$REPO_URL" repo/
echo "<print>Running tests…</print>"
cd repo && go test ./...
echo "<result>tests passed</result>"<print> is not a transition tag — it never appears in
allowed_transitions and does not count toward the "exactly one tag"
requirement. Each <print> also resets the per-state inactivity timer, so a
script that regularly emits progress messages will not trigger a timeout even
if individual steps take longer than the configured window.
LLM states (markdown prompts) support <print> with identical semantics.
Script errors are fatal — no retries, no re-prompting. If a script exits with a non-zero code, outputs no tag, or outputs multiple tags, the workflow terminates. Ensure all code paths emit exactly one valid transition tag.
Provide .sh for Unix and .bat or .ps1 for Windows with the same state name.
Each platform uses its native version:
| Files present | Unix resolves to | Windows resolves to |
|---|---|---|
POLL.sh |
POLL.sh |
Error |
POLL.bat |
Error | POLL.bat |
POLL.ps1 |
Error | POLL.ps1 |
POLL.sh + POLL.bat |
POLL.sh |
POLL.bat |
POLL.sh + POLL.ps1 |
POLL.sh |
POLL.ps1 |
POLL.bat + POLL.ps1 |
Error | Error (ambiguous) |
On Unix, .sh files run with /bin/bash. On Windows, .bat files run with
cmd.exe /c; .ps1 files run with PowerShell.
Every state must end by emitting exactly one of these tags. The tag tells Raymond what to do next.
Transitions to the next state while preserving the current Claude Code session. The agent keeps all context from previous steps.
Implement the feature. When done, emit <goto>COMMIT.md</goto>Use when: The next step needs to see what this step did (e.g., writing a commit message after implementing code).
Context: Preserved. All prior conversation history is visible.
Optional input: Pass data to the target state via {{input}}:
<goto input="3 issues found">REVIEW.md</goto>
Discards the current session and starts the next state with no context.
Create a plan in plan.md. When done, emit <reset>IMPLEMENT.md</reset>Use when: Prior work is captured in files, not needed in context. Useful at phase boundaries (planning → implementation) to keep context clean.
Context: Discarded. The return stack is preserved.
Working directory: Supports an optional cd attribute to change the
agent's working directory:
<reset cd="/repo/worktree-feature">IMPLEMENT.md</reset>
Optional input: Pass data to the target state via {{input}}:
<reset input="phase 2">IMPLEMENT.md</reset>
Calls a child state that runs in a branched context (starts with the
caller's context, then accumulates its own). When the child emits <result>,
control returns to NEXT with the result payload.
Delegate research:
<call return="SUMMARIZE.md">RESEARCH.md</call>The child can iterate, make mistakes, accumulate noise — only the <result>
payload comes back. The caller's context stays clean.
Use when: A subtask may iterate or produce noise that would pollute the parent's context.
Context: Child branches from caller. Caller's context is preserved and resumed when the child returns.
Optional input: Pass initial data to the child state via {{input}}:
<call return="SUMMARIZE.md" input="focus on security">RESEARCH.md</call>
Like <call>, but the child runs with no context (fresh session). Good
for evaluators and decision points.
<function return="NEXT.md">EVALUATE.md</function>Use when: You need a cheap, isolated evaluation. The task requires no context beyond what's in its own prompt.
Context: Child starts fresh. Caller's context is preserved and resumed.
Optional input: Pass data to the child state via {{input}}:
<function return="NEXT.md" input="test output: 3 failures">EVALUATE.md</function>
Spawns a new independent agent running WORKER while the current agent
continues at NEXT. The spawned agent has its own lifecycle and doesn't
return a result to the parent.
<fork next="DISPATCH-ANOTHER.md" item="issue-123">WORKER.md</fork>Attributes: Beyond the required next, any additional attributes become
template variables in the worker's prompt. The cd and input attributes
are consumed by the orchestrator and are not passed as template variables.
<fork next="CONTINUE.md" cd="/repo/worktree-a" task="refactor">WORKER.md</fork>Optional input: Pass initial data to the worker via {{input}}:
<fork next="CONTINUE.md" item="issue-123" input="high priority">WORKER.md</fork>
Use when: You want parallel execution. The parent doesn't wait for the worker and doesn't receive its result.
Returns a payload to the most recent caller (from <call> or <function>),
or terminates the agent if there's no caller.
<result>Analysis complete: found 3 issues</result>The payload text is passed as-is to the return state via {{input}}.
Suspends the agent and requests human input. The text inside the tag is the
human-facing prompt — what the human sees and responds to. When input arrives,
the agent transitions to NEXT with the response available as {{input}}.
<ask next="HANDLE_DECISION.md" timeout="48h" timeout_next="ESCALATE.md">
I have completed my analysis of the three proposals.
My recommendation is Vendor B based on support requirements.
Please respond with your decision:
- "approve A/B/C" to select a vendor
- "reject all" to restart sourcing
- "more info [question]" to request additional analysis
</ask>Attributes:
next(required) — state to transition to when input arrivestimeout(optional) — duration string (e.g.,30m,24h,7d)timeout_next(optional) — state to transition to on timeout; if omitted and the timeout elapses, the workflow fails
Use when: The workflow needs a human decision, approval, or external input before it can continue.
Context: Preserved. The LLM session survives the ask (like goto), so
the agent has full history when it enters next.
Prompt authoring: The tag content should be self-contained — include all context the human needs to make their decision. Text before the tag is internal reasoning visible in logs but not shown to the human.
Timeout handling: When a timeout elapses, the agent transitions to
timeout_next with {{input}} empty. Use this for escalation, fallback, or
cleanup:
---
allowed_transitions:
- tag: ask
next: HANDLE_DECISION.md
timeout: "48h"
timeout_next: ESCALATE.md
---
Analyze the proposals and request a decision from the human.--on-ask CLI flag: By default (--on-ask=reject), workflows that
emit <ask> fail immediately — this protects automated/CI contexts from
hanging. Use --on-ask=pause for interactive use, or run via ray serve
for daemon mode. See skill-packaging.md for the full
exit code protocol.
File attachments: When running under ray serve, <ask> can also
ask the user to upload files or expose files for them to view. Add attributes
on the opening tag:
<ask next="REVIEW.md" upload_slots="resume.pdf:application/pdf">
Please upload your résumé as `resume.pdf`.
</ask>
The runtime stages uploads (and any declared display files) into
<task folder>/asks/<ask_id>/. The state immediately following the
ask receives a {{ask_id}} template variable so it can read the files
back. See workflow-protocol.md for
the full attribute reference, and the Pattern: File Upload from User
example below.
There are two ways to drive additional work from a workflow, and they
have very different lifecycle and visibility consequences — especially
when the parent runs under ray serve.
In-process: <fork> and <fork-workflow> — same orchestrator, same
pool. <fork> spawns another agent within the current workflow;
<fork-workflow> launches a different workflow under the same
orchestrator. Both share the parent's budget, lifecycle, and run pool,
and (under ray serve) appear in the daemon's run list. No separate
state file is created. This is the right choice when the nested work is
logically part of the parent run.
Shelling out (ray <workflow_id> in a shell-script state) — detached,
always CLI pool. A shell step that invokes the ray binary is just
another shell command; the spawned process resolves its own state
directory and writes to .raymond/state/ (the CLI pool) regardless of
where the parent's state lives. The implications for the author:
- The nested run is detached from the parent's lifecycle. If the
parent runs under
ray serve, the nested run does not appear in the daemon's run list, does not stream events through the parent's channel, and is not aborted when the parent is cancelled or quiesced. ray serve --cleandoes not touch the nested run's state file —--cleanonly scopes to the serve pool, and the nested run is in the CLI pool by definition.- The nested process has its own SIGINT handling, its own budget, and its own termination semantics.
Prefer <fork> or <fork-workflow> for in-process orchestration that
stays in the parent's pool. Shell out to ray only when the detachment
is what you want — for example, when driving work in a different project
directory or when the nested run should outlive the parent.
See cross-workflow-design.md for the
<fork-workflow> reference and serve-run-pool.md
for the pool-routing rule the daemon enforces.
| Question | If yes | If no |
|---|---|---|
| Does the child need the caller's context? | call |
function |
| Should intermediate work be discarded? | call or reset |
goto |
| Is this a decision/evaluation point? | function |
Others |
| Will there be messy iterations? | call |
goto or reset |
| Does the next step need this step's history? | goto |
function, call, or reset |
| Is prior work saved to files? | reset |
goto |
| Need parallel execution? | fork |
Others |
| Need a human decision before continuing? | ask |
Others |
| Nested work that's part of this run? | fork / fork-workflow (in-pool) |
shell-out ray <wf> (detaches, CLI pool) |
1. [function] Evaluator: "Which issue to work on?" → "issue 195"
2. [goto] Main session: "Work on issue 195" (receives result)
3. [call] "Create and refine plan" (iterates 3x, returns plan)
4. [reset] "Implement per plan-195.md" (fresh context, reads from file)
5. [call] "Implement until tests pass" (iterates 5x, returns)
6. [goto] "Review and commit" (preserves implementation context)
7. [function] "Ready to commit?" → YES
8. [goto] "Commit and close issue"
9. [result] Workflow complete
Step 4 uses <reset> because the plan is in a file — no need to carry
planning iterations in context. Steps 6 and 8 use <goto> because the commit
message needs to see what was implemented.
Prompts support {{variable}} placeholders that the orchestrator substitutes
before sending to Claude Code.
{{input}} is set when a state receives data from any of these sources:
- A
<call>or<function>child returning via<result> - The
inputattribute on any transition tag (<goto>,<reset>,<call>,<function>,<fork>) - The
--inputCLI flag (sets{{input}}for the first state)
The research findings:
{{input}}
Write a summary based on these findings.The --input CLI flag sets {{input}} for the first state:
ray workflow.md --input "hello, there"{{workflow_id}} contains the unique ID string assigned to the current workflow
run (e.g. workflow_2024-01-15_12-30-45-123456). It is always set — never
empty — while the workflow is running.
It is available in:
- Markdown body prompts
- Implicit-transition
inputattributes rendered by the executor - The
inputattributes of the cross-workflow transition tags:<call-workflow>,<function-workflow>,<fork-workflow>, and<reset-workflow>
Note:
{{workflow_id}}is not substituted in theinputattributes of the within-workflow tags<goto>,<reset>,<call>,<function>, or<fork>— those passinputas a raw string without template rendering.
Workflow ID: {{workflow_id}}
Write your analysis to `outputs/{{workflow_id}}/report.md`.{{agent_id}} contains the current agent's ID string (e.g., main,
main_worker1, main_worker1_analyz1). It is always set — never empty —
while the workflow is running.
It is available in:
- Markdown body prompts
- Implicit-transition
inputattributes rendered by the executor - The
inputattributes of the cross-workflow transition tags:<call-workflow>,<function-workflow>,<fork-workflow>, and<reset-workflow>
Note:
{{agent_id}}is not substituted in theinputattributes of the within-workflow tags<goto>,<reset>,<call>,<function>, or<fork>— those passinputas a raw string without template rendering.
Note for
<fork-workflow>:{{agent_id}}evaluates to the parent agent's ID at state-load time, not any subsequently-spawned child agent's ID.
Agent: {{agent_id}}
Write your results to `outputs/{{agent_id}}/result.md`.This is especially useful in multi-agent workflows to avoid file-path collisions — each agent writes to its own scoped output directory.
{{task_folder}} contains the absolute path to the agent's task folder — a
private directory the orchestrator assigns for the agent to write artifacts,
intermediate state, and (in daemon mode) input attachments. The default
layout is .raymond/tasks/<workflow_id>/<agent_id>.
Write the generated report to `{{task_folder}}/report.md`.{{ask_id}} is bound only in the state running immediately after an
<ask> resolves. It carries the input ID generated by the runtime when the
ask was entered, which is also the name of the per-input subdirectory under
{{task_folder}}/asks/. Use it to read uploaded or staged files:
Read uploaded files from `{{task_folder}}/asks/{{ask_id}}/`.The variable is cleared before the next state runs (one transition deep). If
a workflow needs the ID further along, plumb it forward explicitly through
the transition input attribute or by writing it to a file in the post-ask
state.
{{context_tokens}} is the number of tokens in this agent's session as of the
end of the previous turn — every input-side token (prompt plus cached
prefix) plus that turn's output. Because it is rendered before the current turn
runs, it does not include the prompt about to be sent.
It is a property of the session, so it tracks session identity:
<goto>(same session) — carries forward.<reset>,<function>, fresh-session forks —0(the session starts empty).<call>— the callee inherits the caller's value (a call forks the caller's session).<result>— restored to the caller's value at the time of the call, so a state that returns from a heavy<call>sees the caller's own (smaller) context, not the callee's.
It is 0 until the first turn of a session completes — including when the
first state of a workflow is a script. Use it to branch on how full the context
is getting (e.g. summarize-and-<reset> for compaction, or <result> to
return from a call before the window fills):
Context so far: {{context_tokens}} tokens.
If this is over ~150000, write a concise summary of progress to
`{{task_folder}}/summary.md` and emit `<reset input="resume from summary">`.
Otherwise continue and emit `<goto>NEXT.md</goto>`.The same number is available to script states as RAYMOND_CONTEXT_TOKENS. A
script can also read the on-disk count for any session — including its own via
RAYMOND_SESSION_ID — with ray --claude_context_for_session=<session-id>,
which prints the current context-token count as a bare integer.
Backend note: populated for the Claude Code backend. Under the
pibackend it is currently always0.
{{wf_parent}} is the absolute path of the directory that contains the
workflow. For all local scope types this is the directory holding the workflow
file or folder:
| Workflow source | {{wf_parent}} |
|---|---|
/srv/wf/my_wf.yaml (single-file YAML) |
/srv/wf |
/srv/wf/my_wf.zip (zip archive) |
/srv/wf |
/srv/wf/my_wf/ (folder of state files) |
/srv/wf |
Use it to reference guidance documents that ship alongside the workflow, without hard-coding a path or "installing" anything into the agent — a lightweight, workflow-local alternative to skills:
Before using the deploy tool, read the usage notes at
`{{wf_parent}}/docs/deploy_tool.md`, then proceed.Lay the files out as siblings of the workflow so the reference resolves:
/srv/wf/
my_wf.yaml (or my_wf/ folder of state files)
docs/
deploy_tool.md
The same path is available to script states as RAYMOND_WF_PARENT.
Note: remote (URL-fetched) workflows are out of scope — there the value points into the local cache and is not guaranteed to be meaningful.
Extra attributes on <fork> tags become template variables in the worker:
<!-- Parent emits: -->
<fork next="CONTINUE.md" item="issue-123" priority="high">WORKER.md</fork><!-- WORKER.md receives: -->
Your assigned item is: {{item}}
Priority: {{priority}}For shell scripts, fork attributes become environment variables instead:
echo "Processing item: $item" # "issue-123"
echo "Priority level: $priority" # "high"Note: The next, cd, and input attributes are consumed by the
orchestrator and are not available as template variables or environment
variables.
Transition targets can omit the file extension:
<goto>POLL</goto>The orchestrator resolves the name by checking (in order):
On Unix: POLL.md → POLL.sh
On Windows: POLL.md → POLL.ps1 → POLL.bat
If both .md and a script exist for the same name on the same platform, that's
an ambiguity error. If you specify an explicit extension
(<goto>POLL.sh</goto>), no resolution occurs — that exact file must exist.
This means you can swap a state between markdown and script without updating any transitions that reference it.
By default, all agents execute in the directory where ray was launched.
The cd attribute lets agents operate in different directories:
<!-- Fork a worker into a different directory -->
<fork next="CONTINUE.md" cd="/repo/worktree-a">WORKER.md</fork>
<!-- Reset with a directory change -->
<reset cd="/repo/worktree-feature">IMPLEMENT.md</reset>Supported on: <fork> (sets worker's directory) and <reset> (changes
current agent's directory).
Not supported on: <goto>, <call>, <function> — these continue or
branch existing sessions tied to the original directory.
Relative paths resolve against the agent's current working directory (or the
orchestrator's directory if none is set). Once set, the directory persists
across subsequent transitions until changed by another <reset cd="...">.
If the agent emits no tag, the wrong tag, or multiple tags, the orchestrator generates a reminder listing all valid transitions and re-prompts (up to 3 retries). This is why frontmatter is recommended — it enables recovery.
Missing or invalid tags are fatal. The orchestrator has no way to generate a meaningful reminder without knowing the allowed transitions.
All errors are fatal. Scripts must emit exactly one valid tag on every code path. There is no re-prompting for scripts.
workflows/coding/
PLAN.md
IMPLEMENT.md
PLAN.md:
---
allowed_transitions:
- { tag: reset, target: IMPLEMENT.md }
---
Read the requirements and create a detailed plan in plan.md.
STOP after writing plan.md. Do not start implementing yet — that happens
in a later step.IMPLEMENT.md:
---
allowed_transitions:
- { tag: goto, target: IMPLEMENT.md }
- { tag: result }
---
Implement the feature per plan.md. Run the tests.
If tests fail, fix and retry: <goto>IMPLEMENT.md</goto>
When all tests pass: <result>Done</result>---
model: haiku
allowed_transitions:
- { tag: result }
---
Given the following test output, is the task complete?
{{input}}
Respond with <result>YES</result> or <result>NO</result>workflows/monitor/
POLL.sh # Zero-cost polling loop
PROCESS.md # LLM reasoning when work is found
POLL.sh:
#!/bin/bash
response=$(curl -s "$API_URL/tasks?status=pending")
count=$(echo "$response" | jq 'length')
if [ "$count" -gt 0 ]; then
echo "$response" > /tmp/pending_tasks.json
echo "<goto>PROCESS.md</goto>"
else
sleep 60
echo "<reset>POLL.sh</reset>"
fiPROCESS.md:
---
allowed_transitions:
- { tag: reset, target: POLL.sh }
- { tag: result }
---
Read /tmp/pending_tasks.json and process the tasks.
When done, go back to polling: <reset>POLL.sh</reset>
If there's nothing more to do: <result>All tasks processed</result>workflows/dispatch/
DISPATCH.md
WORKER.md
DISPATCH.md:
Read items.txt. For each item, spawn a worker:
<fork next="DISPATCH.md" item="item-name">WORKER.md</fork>
When there are no more items: <result>All items dispatched</result>WORKER.md:
---
allowed_transitions:
- { tag: result }
---
Process item: {{item}}
When done: <result>Processed {{item}}</result>workflows/approval/
ANALYZE.md # LLM analyzes the proposal
REVIEW.md # Asks human decision
APPROVED.md # Proceeds with approved proposal
ESCALATE.md # Handles timeout
ANALYZE.md:
---
allowed_transitions:
- { tag: goto, target: REVIEW.md }
---
Read the proposal documents in the working directory and prepare a summary
with a recommendation.
STOP after writing your analysis. Do not make the decision yet — a human
reviewer will decide in a later step.REVIEW.md:
---
allowed_transitions:
- tag: ask
next: APPROVED.md
timeout: "48h"
timeout_next: ESCALATE.md
---
Present your analysis to the human and request their decision. Include all
relevant context in the ask prompt so the reviewer can decide without
reading the raw documents.APPROVED.md:
---
allowed_transitions:
- { tag: result }
---
The reviewer's decision: {{input}}
Execute the decision and produce a final report.Daemon-mode (ray serve) workflows can ask the user to upload a file
through the web UI. Declare an upload affordance on the <ask> tag, then
read the uploaded file from the input subdirectory in the next state.
workflows/upload-resume/
REQUEST.md # Asks the user to upload a file
ANALYZE.md # Reads the uploaded file
REQUEST.md:
---
allowed_transitions:
- tag: ask
next: ANALYZE.md
upload_slots: "resume.pdf:application/pdf"
---
We're collecting résumés for review.
Emit:
<ask next="ANALYZE.md" upload_slots="resume.pdf:application/pdf">
Please upload your résumé as a PDF named `resume.pdf`. Add any notes you'd
like the reviewer to see.
</ask>ANALYZE.md:
---
allowed_transitions:
- { tag: result }
---
The user uploaded a résumé. Their notes:
{{input}}
Read the file at `{{task_folder}}/asks/{{ask_id}}/resume.pdf`,
summarize the candidate's experience in 3 bullet points, and emit:
<result>summary written</result>The slot mode declaration tells the UI to render a single labeled upload
control for resume.pdf, and constrains uploads to PDF content type. For
an open-ended affordance ("attach any supporting files") use bucket mode:
<ask next="REVIEW.md"
upload_bucket="true"
upload_max_count="5"
upload_mime="image/png,image/jpeg">
Attach up to 5 supporting screenshots (PNG or JPEG).
</ask>
In bucket mode, the agent discovers the actual filenames by listing
{{task_folder}}/asks/{{ask_id}}/ in the next state.
A pi workflow that audits a directory using a tight read-only tool surface.
The tools allowlist omits bash, write, and edit so the audit cannot
modify anything even if the agent tries.
workflows/audit/
workflow.yaml
START.md
REPORT.md
workflow.yaml:
name: Read-only directory audit
backend:
name: pi
options:
tools: [read, grep, find, ls]
thinking: medium
default_budget: 0.50
input:
mode: required
label: Path to auditSTART.md:
---
allowed_transitions:
- { tag: goto, target: REPORT.md }
---
You are auditing the directory at `{{input}}`.
Use `find`, `ls`, `read`, and `grep` to survey the layout. Catalogue:
- top-level structure
- any files larger than 1 MB
- any files matching `*.env`, `*.key`, or `*.pem`
STOP after gathering the data. Do not write any files or attempt edits —
the tool surface is read-only. After surveying, emit:
<goto>REPORT.md</goto>REPORT.md:
---
allowed_transitions:
- { tag: result }
---
Summarise the audit findings as a short markdown report covering:
- directory layout (one paragraph)
- large files (bullet list, sizes in MB)
- sensitive-looking files (bullet list, with the path and a brief note)
Reply with the report wrapped in a single <result>...</result> tag.Run it from the CLI:
ray run ./workflows/audit --input /path/to/audit
Or via the daemon (the dashboard will show a "pi" badge on the workflow
card and the per-state pi session id in the live log so you can resume
the session manually with pi --session <id> if needed):
ray serve --root ./workflows
For more complete examples, see sample-workflows.md.