What flow can do today. Each capability is exercised by the shipped examples and specified precisely in the Reference.
Modeling
Typed inputs/outputs wired by explicit edge mappings — no shared flat state.
A port declares a JSON Schema (scalar or nested object/array) plus one `required` flag.
Port values are live Python (int/float/bool/list/dict), not stringified — structure flows between nodes intact.
Reserved source + sink: state seeds $in; edges to $output collect the result. `entry` is optional (derived from $in).
equals / contains / numeric gt / gte / lt / lte / and / or / not guards over a source output port.
Back-edges declare loop.max so cycles terminate; an unbounded cycle fails validation with its path; a loop with no `when` guard warns.
Inline code or a run: module:func reference, imported with the workflow dir on path.
A type:"subflow" node runs another workflow as one opaque node — inline or a `./child.json` path ref; ports are derived from the child's signature.
An edge requires the source and destination port types to match — including a sub-field's type via its schema.
$in carries an optional in_schema (else it's inferred from consumers); the $output signature is derived from the sink edges — so a workflow has a checkable I/O type.
Unknown ports, unfed inputs, ambiguous producers, unbounded cycles, port-type mismatches, prompt-typo interpolation all fail fast.
Data flow
A prompt reads a field with `{{ $.plan.tasks[0] }}`; both engines share one jsonpath-ng evaluator.
Because ports are declared, every `{{ $.key }}` in a prompt or condition is checked at load time — a typo is a fail-fast, not a silently dropped section.
An edge maps a nested field: `"map": {"$.verdict.within_budget": "flag"}`, type-checked against the source schema.
Initial state carries type-native JSON; a structured seed stays a dict/list, not a Python repr.
Execution
Interpreter and generated Python run the same completion-driven frontier scheduler: stable parallel readiness, conditional inputs, AND joins, and heterogeneous bounded-loop groups.
A fan_out edge maps a node over a runtime-sized array (once per element, in parallel); a fan_in edge gathers the results into an index-ordered list.
A node is a pure function (provider, ctx, inputs → outputs); a generic driver owns guards, retry, store, memo, budget, checkpoint.
max_concurrency bounds how many nodes run at once; a separate fan_max_concurrency bounds instances within one fan-out (both default to unlimited).
CLI and generated Python print one success/message/output/context envelope with workflow/run id, timing, tokens, and last node; execute() retains the detailed internal runtime container.
on_event streams NodeStarted / NodeFinished / NodeFailed with per-node duration and tokens.
A MetricsCollector consumes the event stream into a per-node + per-run snapshot (runs, duration, tokens, failures).
execute(max_tokens=N) aborts a run with WorkflowBudgetExceeded once cumulative agent tokens pass the ceiling.
Run with no LLM calls; agent nodes echo DRYRUN:<model> to test wiring.
Resilience
A RetryPolicy(max, backoff) retries a failed node before giving up (default: fail-fast). Retry lives in the driver.
on_error:isolate records a failed branch in runtime.failed and skips only its sub-tree.
A CheckpointStore persists coherent frontier batches by run id; resume restores outputs, completion, loop counters, trace, memo, and token usage with at-least-once batch semantics.
A human node pauses awaiting a named signal; deliver it and resume the run to continue.
deterministic:true memoises output by (node, input hash) for safe retry/resume.
Agents & tools
An agent fans its submit_result object across several typed output ports by field name — each wired independently.
An agent's structured-output contract is derived from its output ports (no separate output_schema), validated by fastjsonschema.
Agent nodes can enable a built-in web_search tool with its own browsing model.
A module:func tool manifest, loaded at run and generate time.
An agent node can set backend:"claude-cli"/"codex-cli" to run one turn by shelling out to a coding-agent CLI instead of the in-process SDK — no provider/API key needed (the CLI owns auth); structured output maps to the CLI's native schema flag.
A CLI agent node NARROWS the CLI's own toolset with allowed_tools (built-ins or mcp__server__tool); flow ships no tools. An empty list runs the tightest sandbox.
A CLI agent node declares mcp_servers (an opaque pass-through spec); flow generates the CLI's MCP config, with ${ENV} secret interpolation — the JSON carries the reference, never the token.
Scheduling
A schedule:{mode:"timer"} block fires the workflow on an interval or cron expression; xdog-flow scheduling install writes a systemd user timer (or crontab fallback).
A schedule:{mode:"hook"} block fires on an external event (http/file), delivering a signal to a fresh run — reusing the human-node pause/resume primitive.
All hook workflows on a host share ONE systemd-supervised listener that routes each event (http by path, file by dir) to the right bundle — no port collisions, no per-workflow daemon.
xdog-flow scheduling install/uninstall/list manages bundles and OS units through a local registry; --dry-run previews changes without touching the OS.
Codegen & authoring
Compile a workflow JSON to a runnable, ruff-clean Python module that mirrors the interpreter node-for-node.
Interpreter and generated module agree node-for-node — enforced by a cross-engine parity suite on every feature.
generate --portable emits a self-contained dir; ai/agent are vendored only when an SDK agent node needs them, so a pure-CLI or script-only bundle drops them (requirements trim to jsonpath-ng). --offline downloads wheels for a no-network install.
The generated module honours FLOW_INPUTS (JSON merged into $in) and FLOW_PROVIDER — parity with the interpreter's --input / --provider.
xdog-flow build edits the same Git-friendly JSON that Coding Agents generate through the Flow skill; a local Web UI is the next editor surface.
Text listing, layered ASCII, Graphviz SVG (with fallback), and Mermaid; node boxes are colour-coded by type (agent/script/human/subflow).
Testing
xdog-flow test runs a workflow's <name>.test.json — a workflow path, a suite, or a whole directory; exit 1 on failure drops it into a pre-commit hook or CI.
Agent turns (SDK and CLI alike), human signals, and whole subflow nodes are stubbable; script nodes need an explicit --allow-script-stub. Edges, conditions, loops, fan-out, coercion and $output collection always run for real.
A stub is injected at the provider call, after prompt interpolation and before output parsing — so it is validated by the node's own required-field check and coercion, and a broken {{ $.path }} still fails.
The stub runner answers every agent node whatever its backend, and no provider is constructed in test mode; an unstubbed agent node fails loudly instead of dialling out.
A stub rule selects on when (deep-subset match on inputs), index (fan array position), or round (activation ordinal) — never on completion order, so a fan-out case is not flaky.
expect takes one of success / error substring / paused-at-node, plus a deep-subset output match and a calls map that covers executed and skipped nodes, fan instances, and loop iterations in one number.
A stub aimed at a missing node, the wrong node type, or an undeclared output port fails before anything executes — and a selector that never fires is a failure, not a silent fall-through.