x-dog

Tools for building AI agents and managing LLM deployments.

GitHub →

x-dog / Packages / flow / Features

What flow can do today. Each capability is exercised by the shipped examples and specified precisely in the Reference.

Modeling

Node-private ports
Typed inputs/outputs wired by explicit edge mappings — no shared flat state.
JSON Schema ports
A port declares a JSON Schema (scalar or nested object/array) plus one `required` flag.
Type-native wire
Port values are live Python (int/float/bool/list/dict), not stringified — structure flows between nodes intact.
$in / $output nodes
Reserved source + sink: state seeds $in; edges to $output collect the result. `entry` is optional (derived from $in).
Conditional edges
equals / contains / numeric gt / gte / lt / lte / and / or / not guards over a source output port.
Bounded loops + cycle detection
Back-edges declare loop.max so cycles terminate; an unbounded cycle fails validation with its path; a loop with no `when` guard warns.
Script nodes
Inline code or a run: module:func reference, imported with the workflow dir on path.
Sub-workflows
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.
Edge type checking
An edge requires the source and destination port types to match — including a sub-field's type via its schema.
Typed workflow signature
$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.
Validate before running
Unknown ports, unfed inputs, ambiguous producers, unbounded cycles, port-type mismatches, prompt-typo interpolation all fail fast.

Data flow

JSONPath interpolation
A prompt reads a field with `{{ $.plan.tasks[0] }}`; both engines share one jsonpath-ng evaluator.
Strict interpolation
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.
Sub-field edge mapping
An edge maps a nested field: `"map": {"$.verdict.within_budget": "flag"}`, type-checked against the source schema.
Structured $in seed
Initial state carries type-native JSON; a structured seed stays a dict/list, not a Python repr.

Execution

Shared frontier kernel
Interpreter and generated Python run the same completion-driven frontier scheduler: stable parallel readiness, conditional inputs, AND joins, and heterogeneous bounded-loop groups.
Dynamic fan-out
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.
Pure node + driver
A node is a pure function (provider, ctx, inputs → outputs); a generic driver owns guards, retry, store, memo, budget, checkpoint.
Concurrency caps
max_concurrency bounds how many nodes run at once; a separate fan_max_concurrency bounds instances within one fan-out (both default to unlimited).
Structured run result
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.
Structured event stream
on_event streams NodeStarted / NodeFinished / NodeFailed with per-node duration and tokens.
Metrics aggregation
A MetricsCollector consumes the event stream into a per-node + per-run snapshot (runs, duration, tokens, failures).
Cost budget
execute(max_tokens=N) aborts a run with WorkflowBudgetExceeded once cumulative agent tokens pass the ceiling.
Offline dry-run
Run with no LLM calls; agent nodes echo DRYRUN:<model> to test wiring.

Resilience

Per-node retry
A RetryPolicy(max, backoff) retries a failed node before giving up (default: fail-fast). Retry lives in the driver.
Failure isolation
on_error:isolate records a failed branch in runtime.failed and skips only its sub-tree.
Checkpoint & resume
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.
Human-in-the-loop
A human node pauses awaiting a named signal; deliver it and resume the run to continue.
Deterministic reuse
deterministic:true memoises output by (node, input hash) for safe retry/resume.

Agents & tools

Agent multi-output ports
An agent fans its submit_result object across several typed output ports by field name — each wired independently.
Derived output schema
An agent's structured-output contract is derived from its output ports (no separate output_schema), validated by fastjsonschema.
Agent web search
Agent nodes can enable a built-in web_search tool with its own browsing model.
JSON-declared custom tools
A module:func tool manifest, loaded at run and generate time.
CLI agent backend
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.
CLI tool allow-list
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.
Per-node MCP servers
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

Active / timer 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).
Passive / hook scheduling
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.
Shared hook listener
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.
Scheduling lifecycle
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

Code generation
Compile a workflow JSON to a runnable, ruff-clean Python module that mirrors the interpreter node-for-node.
Interpret == compile
Interpreter and generated module agree node-for-node — enforced by a cross-engine parity suite on every feature.
Portable bundle
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.
Runtime overrides
The generated module honours FLOW_INPUTS (JSON merged into $in) and FLOW_PROVIDER — parity with the interpreter's --input / --provider.
Human + Agent authoring
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.
Four diagram renderers
Text listing, layered ASCII, Graphviz SVG (with fallback), and Mermaid; node boxes are colour-coded by type (agent/script/human/subflow).

Testing

Companion test suites
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.
Stub only the boundaries
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.
Stubs on the production path
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.
No accidental network
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.
Concurrency-safe selectors
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.
Three-way outcome + calls
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.
Suite validated at load
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.