Current Flow capabilities and their limits, grounded in the typed workflow runtime and CLI. Generation is not universal dependency freedom; see Reference for deployment caveats.
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.
Cycles use bounded loop.max with optional when, or strict while with its own condition/bound; exhaustion is a normal bounded stop or strict non-convergence error.
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 required inputs, ambiguous producers, unbounded cycles, schema mismatches and prompt-root typos fail before node execution; custom imports may still have effects.
Data flow
A prompt reads a field with `{{ $.plan.tasks[0] }}`; both engines share one jsonpath-ng evaluator.
Prompt/condition roots are checked against declared ports; supported JSONPath sub-fields use the source schema, but not every complex path is statically typable.
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); fan_in:list preserves index-ordered values and fan_in:concat flattens array results one level. The whole fan group is one checkpoint unit.
Node functions consume typed inputs and return outputs; a driver owns retry, storage, memo, budget and checkpoint. Scripts/tools may still have external effects.
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 --dry-run echoes DRYRUN:<model> for SDK nodes only; scripts/CLI backends still execute, structured submission may fail, and generated modules have no equivalent switch.
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 reuses recorded output by (node, input hash); it does not roll back effects or close an effect/checkpoint crash window.
Agents & tools
Each executor creates a fresh registry: echo, SDK defaults bash/filesystem/current_time, and opt-in submit_result. Node tool selection and structured injection use the existing Agent loop; no global runtime inventory.
SDK and generated SDK nodes pass generic result_schema/result_sink; validation success writes result_sink['result']. A single structured port accepts its native value, multiple ports split an object; CLI backends use native schema flags.
The application-scoped flow capability invokes non-interactive public CLI argv without a shell and returns exit_code/stdout/stderr. No Flow daemon; manual build stays outside the tool. Normal host permissions apply.
Installed metadata advertises the flow skill without loading its body or granting tools. Explicit activation (including a node's skills list) loads instructions; discovery starts no workflow or process.
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.
SDK 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.
Nonempty allowed_tools is forwarded to Claude; Codex has no per-tool allowlist in this adapter. Empty does not mean verified deny-all. External CLI tools/MCP are separate from Flow's SDK registry.
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 Linux systemd user timer; the CLI does not install an automatic crontab fallback.
A schedule:{mode:"hook"} block configures event-to-signal routing. The live entry point serves loopback HTTP; accepted file/socket metadata is not deployed watchers/socket activation.
Hook entries share one listener, but the rendered module name and /usr/bin/python3 environment require operator correction/review. HTTP 202 after spawning is not durable execution or workflow completion; no built-in HTTP auth.
scheduling install/list/start/stop/uninstall manages local registry and systemd state. --dry-run skips deployment but still loads trusted code; list is registry facts, not live next-fire status.
Codegen & authoring
Compile typed JSON to Python with inlined helpers and shared graph semantics. SDK nodes import xdog.agent/xdog.ai; subflows call xdog.flow at runtime; custom imports still apply.
Interpreter and generated module agree node-for-node — enforced by a cross-engine parity suite on every feature.
generate --portable vendors selected sources: SDK nodes/tool manifests select ai/agent, subflows add flow. This is not a recursive child/custom dependency closure; inspect requirements. --offline downloads selected wheels at build time.
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.
StubRunner answers every agent node, including CLI backends; missing stubs fail, not dial out. Unstubbed scripts and imported custom code can still use files, processes and the network.
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.