Skip to content

Latest commit

 

History

History
1271 lines (968 loc) · 40.7 KB

File metadata and controls

1271 lines (968 loc) · 40.7 KB

Workflow Authoring Guide

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.

Concepts

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 (.sh on Unix, .bat or .ps1 on 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.

Choosing a Backend

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 / --model selectors.

Shell-script states do not use any backend — they execute directly.

Declaring the backend

A workflow opts into the pi backend in its manifest. Without a backend: declaration, the workflow uses Claude.

# workflow.yaml
backend: pi

For 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.

Tool safety

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.

Cross-workflow with different backends

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.

What's different about pi

  • 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 --extension and --skill mechanisms. 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

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.

Scoping: Tell the Agent When to Stop

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:

  1. State what to do. Be specific about the task for this step.
  2. 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>".
  3. 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.

YAML Frontmatter

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.

Model Selection

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.

Effort Level

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 Script States

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>"
fi

Scripts 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

Environment Variables

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

Persisting Data Between Script Runs

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>"
fi

Streaming Intermediate Output (<print>)

Scripts 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.

Error Handling

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.

Cross-Platform Scripts

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.

Transition Tags

Every state must end by emitting exactly one of these tags. The tag tells Raymond what to do next.

<goto>FILE</goto> — Continue in Same Context

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>

<reset>FILE</reset> — Fresh Start

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>

<call return="NEXT">CHILD</call> — Subroutine Call

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>

<function return="NEXT">EVAL</function> — Stateless Evaluation

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>

<fork next="NEXT" ...>WORKER</fork> — Spawn Independent Agent

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.

<result>...</result> — Return or Terminate

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}}.

<ask next="NEXT" ...>prompt</ask> — Request Human 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 arrives
  • timeout (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.

Nested Work: In-Process vs Shelling Out to ray

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 --clean does not touch the nested run's state file — --clean only 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.

Choosing the Right Pattern

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)

Combined Example

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.

Template Variables

Prompts support {{variable}} placeholders that the orchestrator substitutes before sending to Claude Code.

{{input}} — Return Values and Input

{{input}} is set when a state receives data from any of these sources:

  • A <call> or <function> child returning via <result>
  • The input attribute on any transition tag (<goto>, <reset>, <call>, <function>, <fork>)
  • The --input CLI 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}} — Workflow Identifier

{{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 input attributes rendered by the executor
  • The input attributes of the cross-workflow transition tags: <call-workflow>, <function-workflow>, <fork-workflow>, and <reset-workflow>

Note: {{workflow_id}} is not substituted in the input attributes of the within-workflow tags <goto>, <reset>, <call>, <function>, or <fork> — those pass input as a raw string without template rendering.

Workflow ID: {{workflow_id}}

Write your analysis to `outputs/{{workflow_id}}/report.md`.

{{agent_id}} — Agent Identifier

{{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 input attributes rendered by the executor
  • The input attributes of the cross-workflow transition tags: <call-workflow>, <function-workflow>, <fork-workflow>, and <reset-workflow>

Note: {{agent_id}} is not substituted in the input attributes of the within-workflow tags <goto>, <reset>, <call>, <function>, or <fork> — those pass input as 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}} — Per-Agent Working Area

{{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}} — Identifier of the Resolving Ask

{{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}} — Context-Window Fullness

{{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 pi backend it is currently always 0.

{{wf_parent}} — Workflow's Containing Directory

{{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.

Fork Attributes

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.

State Resolution

Transition targets can omit the file extension:

<goto>POLL</goto>

The orchestrator resolves the name by checking (in order):

On Unix: POLL.mdPOLL.sh

On Windows: POLL.mdPOLL.ps1POLL.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.

Working Directory (cd Attribute)

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="...">.

Error Handling

Markdown states with frontmatter

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.

Markdown states without frontmatter

Missing or invalid tags are fatal. The orchestrator has no way to generate a meaningful reminder without knowing the allowed transitions.

Shell scripts

All errors are fatal. Scripts must emit exactly one valid tag on every code path. There is no re-prompting for scripts.

Complete Examples

Pattern: Plan then Implement

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>

Pattern: Evaluator Decision Point

---
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>

Pattern: Hybrid Script + LLM Workflow

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>"
fi

PROCESS.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>

Pattern: Fork Workers

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>

Pattern: Human Approval Gate

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.

Pattern: File Upload from User

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.

Pattern: Pi Workflow with Constrained Tools

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 audit

START.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.