Directory-based workflows require one file per state — a natural fit for large
workflows, but heavyweight for small ones. A three-state workflow needs a
directory, three .md files, and possibly a few .sh scripts. YAML workflows
let you define the entire workflow in a single file:
states:
1_START:
prompt: |
Greet the user, then transition.
allowed_transitions:
- tag: goto
target: DONE.md
DONE:
prompt: |
Say goodbye.
allowed_transitions:
- tag: resultAny CLI command that accepts a directory or ZIP scope also accepts a .yaml or
.yml file.
A YAML workflow file has a single required top-level key, states, whose value
is a mapping of state names to state definitions:
states:
STATE_NAME:
# ... state definition (see below)Constraints:
statesmust be present, must be a mapping, and must contain at least one entry.- State names must be non-empty strings and must not contain path separators
(
/or\). - Duplicate state names are not allowed.
A markdown state has a prompt key and optional policy fields:
states:
REVIEW:
prompt: |
Review the code for correctness.
Focus on edge cases and error handling.
allowed_transitions:
- tag: goto
target: FIX.md
- tag: result
model: sonnet
effort: high| Field | Type | Required | Description |
|---|---|---|---|
prompt |
string | yes | The prompt text sent to the model (supports {{input}} and other template variables — see authoring-guide.md) |
allowed_transitions |
list | no | Constrains which transitions the model may use |
model |
string | no | Model override (opus, sonnet, haiku) |
effort |
string | no | Extended thinking level (low, medium, high) |
timeout |
number | no | Per-state timeout in seconds. Overrides the workflow-level timeout and the run-level default (--timeout / config) for this state. 0 means no timeout. Negative values are a validation error. |
force_implicit |
bool | no | When true, the orchestrator dispatches the (single) allowed transition without parsing the model's output for tags. Requires the policy to be implicit-eligible. See workflow-protocol.md. |
Each entry in allowed_transitions is a mapping with a tag key and optional
attributes like target, payload, return, etc. — the same attributes used
in directory-based frontmatter. Targets reference virtual filenames (see
Virtual files).
A script state has one or more platform keys (sh, ps1, bat) and no
prompt:
states:
BUILD:
sh: |
#!/usr/bin/env bash
set -euo pipefail
make build 2>&1
bat: |
@echo off
nmake build 2>&1At least one platform key must be present:
| Field | Type | Description |
|---|---|---|
sh |
string | Shell script (Unix) |
ps1 |
string | PowerShell script (Windows) |
bat |
string | Batch script (Windows) |
timeout |
number | Per-state timeout in seconds (same semantics as on markdown states) |
Restrictions: Script states must not have allowed_transitions, model,
effort, or force_implicit — these policy fields apply only to markdown
states.
Each state must be exactly one type — markdown or script. A state with both
prompt and a script key (e.g. sh) is a validation error. A state with
neither is also an error.
A top-level timeout key sets a workflow-wide idle timeout (seconds) applied to
every state that does not declare its own. Unlike the daemon manifest keys
below, it is honoured by both ray <file> (CLI) and ray serve:
timeout: 300 # workflow-wide default for all states
states:
START:
prompt: ...0 disables the timeout; a negative value is a validation error.
Timeout precedence (most specific wins):
- A state's own
timeout:. - The top-level workflow
timeout:. - The run-level default —
--timeout(CLI) or[raymond] timeoutin.raymond/config.toml. - The built-in default (600s).
An author's timeout: (state or workflow level) therefore always beats the
run-level default; --timeout / config act as the fallback for workflows that
don't set one. ray serve resolves that run-level default from the same
[raymond] timeout (or the 600s built-in) as the CLI, so a workflow's effective
timeout is identical whether run via ray <file> or the daemon.
Unrecognized state keys (e.g. a misspelled force_implcit) are tolerated at
runtime so a typo never aborts a run — the key is ignored and the workflow
continues, but a non-fatal warning is emitted to the event stream so it's
visible to anyone watching. ray lint reports the same keys as a
unknown-field warning, which is the intended place to catch them before a
run. The same behavior applies to --- frontmatter in directory-based .md
state files. Top-level manifest keys (name, description, input,
backend, etc.) are not treated as state fields.
Note this is distinct from a recognized field used in the wrong place — e.g.
model or force_implicit on a script state — which remains a hard
validation error, since that's a structural mistake rather than a typo.
YAML workflows present states as virtual files so the rest of the system (orchestrator, lint, diagram) can treat them identically to directory-based workflows:
| State type | Virtual filename(s) |
|---|---|
| Markdown | STATE_NAME.md |
| Script (sh only) | STATE_NAME.sh |
| Script (ps1 only) | STATE_NAME.ps1 |
| Script (bat only) | STATE_NAME.bat |
| Script (multiple) | One file per platform, in order: .sh, .ps1, .bat |
When a markdown state is read, its policy fields are synthesized into YAML
frontmatter — the same format used in directory-based .md files. A state with
no policy fields produces bare prompt text with no frontmatter block.
# Entry point resolved automatically (looks for 1_START or START)
ray workflow.yaml
# Start at a specific state
ray workflow.yaml/REVIEW
# With flags
ray workflow.yaml/REVIEW -m sonnet --input "review this PR"The workflow.yaml/STATE syntax is not a filesystem path — the CLI splits on
the YAML extension to extract the scope and initial state.
ray --resume <workflow-id>On resume, the YAML file is re-parsed and re-validated. If the file has been deleted or become invalid since the workflow was started, the resume fails with a clear error.
ray lint workflow.yaml
ray lint --json --level error workflow.yamlRuns the same static analysis checks as directory-based workflows (missing transition targets, unreachable states, etc.).
ray diagram workflow.yaml
ray diagram --html --output diagram.html workflow.yamlGenerates a Mermaid flowchart or interactive HTML diagram from the workflow states and transitions.
When no initial state is specified (ray workflow.yaml), the entry point
is resolved using the same rules as directory-based workflows:
- Look for a state named
1_START(any extension) - Fall back to a state named
START - If both exist, report an ambiguity error
- If neither exists, report an error
When an initial state is given (ray workflow.yaml/REVIEW), the bare name
is resolved to its virtual filename (REVIEW.md or REVIEW.sh, depending on
the state type).
The YAML scope produces three error types:
| Error | When |
|---|---|
| Parse error | File cannot be read, or YAML syntax is invalid |
| Validation error | YAML is well-formed but violates schema rules (missing states, dual-type state, script with policy fields, etc.) |
| File not found | A requested virtual filename does not correspond to any state |
When states are defined at the root level without a states: wrapper, the
parser detects common state-like keys and suggests adding the wrapper.
A YAML workflow may carry manifest metadata as top-level keys alongside
states. Every well-formed YAML scope file in a configured serve root is
discovered by the daemon (ray serve) — there is no opt-in field. The
workflow id is derived from the filename (including extension), so
vendor-approval.yaml is registered as vendor-approval.yaml.
The explicit
id:field is no longer supported. The workflow id is always derived from the on-disk filename (for yaml-scope and zip archives) or the directory name (for directory-scope workflows). If a manifest declares anid:field, the parser hard-rejects it.
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | "" |
Human-readable display name. |
description |
string | "" |
Description shown in tool/endpoint documentation. |
input |
object | {mode: optional} |
Describes the single optional string passed to the first state as {{input}}. See Input block. |
default_budget |
float64 | 0 |
Default USD budget when callers don't specify one. |
working_directory |
string | "" |
Default working directory for runs. |
environment |
map[string]string | nil |
Environment variables for runs. Supports ${VAR} interpolation. |
requires_human_input |
string | "auto" |
"auto", "true", or "false". Controls whether the daemon rejects the workflow from contexts that cannot deliver human input. At discovery time, auto resolves to false for YAML workflows, matching the behavior for directory and zip scopes. |
A workflow receives at most one string of input at launch time, substituted
into the first state as {{input}}. The input block describes that string
for UI surfaces, and lets the daemon enforce whether input is permitted or
required.
| Field | Type | Default | Description |
|---|---|---|---|
mode |
string | "optional" |
One of required, optional, none. required rejects launches with an empty input; none rejects launches with a non-empty input. |
label |
string | "" |
Short prompt shown in the web UI field. |
description |
string | "" |
Longer help text shown under the input field. |
The entire input block is optional. Omitting it is equivalent to input: {mode: optional} with no label or description.
Adding manifest fields has no effect on CLI behavior. ray workflow.yaml runs the workflow identically whether or not the manifest
fields are present — the CLI ignores them. They are consumed only by the
daemon registry.
The workflow id is the filename. Save the example below as
vendor-approval.yaml and the daemon registers it as vendor-approval.yaml.
name: Vendor Approval
description: Evaluates a vendor proposal and routes through human approval
default_budget: 5.0
input:
mode: required
label: Vendor name
description: The vendor to evaluate (passed to the first state as {{input}}).
requires_human_input: auto
states:
1_START:
prompt: |
Research the vendor "{{input}}" and prepare an analysis covering
price, support SLA, and references.
STOP after writing your analysis. Do not make a recommendation yet.
allowed_transitions:
- tag: ask
next: DECISION.md
timeout: "48h"
timeout_next: ESCALATE.md
DECISION:
prompt: |
The reviewer's decision: {{input}}
Execute the decision and produce a final report in vendor_report.md.
allowed_transitions:
- tag: result
ESCALATE:
prompt: |
The vendor decision timed out. Send an escalation notice and close.
allowed_transitions:
- tag: resultThis file can be run directly (ray workflow.yaml), discovered by the
daemon (ray serve --root .), linted (ray lint workflow.yaml), and
diagrammed (ray diagram workflow.yaml).
| Directory | ZIP | YAML | |
|---|---|---|---|
| One file per state | yes | yes (inside archive) | no — all states in one file |
| Best for | Large workflows, version control | Distribution, immutable snapshots | Small workflows, prototyping |
| Script support | .sh/.bat/.ps1 files |
Same, inside archive | sh/ps1/bat keys in state definition |
| Policy via frontmatter | In each .md file |
Same | allowed_transitions, model, effort, timeout keys |
| Manifest | Separate workflow.yaml file |
workflow.yaml inside archive |
Embedded in same file |
| CLI syntax | ray dir/ or ray dir/FILE.md |
ray archive.zip |
ray workflow.yaml or ray workflow.yaml/STATE |
A workflow that reviews code, optionally fixes issues, then reports results:
states:
1_START:
prompt: |
You are a code reviewer. Analyze the code provided in {{input}}.
If you find issues, transition to FIX. Otherwise, transition to REPORT.
allowed_transitions:
- tag: goto
target: FIX.md
- tag: goto
target: REPORT.md
model: sonnet
effort: high
timeout: 120
FIX:
prompt: |
Apply the fixes you identified. When done, transition to REPORT
with a summary of changes.
allowed_transitions:
- tag: goto
target: REPORT.md
REPORT:
prompt: |
Summarize the review outcome and any changes that were made.
allowed_transitions:
- tag: resultray workflow.yaml --input "$(cat src/main.go)"Transition targets use virtual filenames — .md for markdown states, .sh or
.bat for script states. For example, a transition to a script state named
BUILD would use target: BUILD.sh (Unix) or target: BUILD.bat (Windows).