# Changelog Source: https://docs.pflow.run/changelog Product updates and announcements ## LLM Key Propagation and Diagnostics We resolved an issue in the LLM client layer where validated API keys were not being passed correctly to LiteLLM. Diagnostics have also been improved to surface sanitized provider error details when an API call fails. ```bash theme={null} # Diagnostic output now includes sanitized provider details on failure pflow workflow.pflow.md ``` When an LLM provider call fails, diagnostic rendering now displays the provider's underlying error details with API-key material masked. **Highlights** * Fixed API key handling to ensure the validated key is reliably forwarded to LiteLLM during model invocations. * Enhanced error handling to surface explicit provider error text in diagnostic failure outputs. ## Updated Default Models pflow's existing automatic model selection now targets newer released models for each supported provider. **Highlights** * Updated the Anthropic default from `anthropic/claude-sonnet-4-5` to `anthropic/claude-sonnet-5`. * Updated the Google default from `gemini/gemini-3-flash-preview` to `gemini/gemini-3.5-flash-lite`. * Updated the OpenAI default from `openai/gpt-5.2` to `openai/gpt-5.6-luna`. * Updated the discovery and filtering fallback from Sonnet 4.5 to Sonnet 5. ## Durable Workflow Resumption and Non-TTY Gates We introduced a workflow resumption engine that continues failed or interrupted runs from the failed top-level step while restoring completed upstream outputs. Human approval gates now work in non-interactive (non-TTY) environments by saving execution state to the streamable trace file and issuing durable tokens that can be answered later. ```bash theme={null} # List all pending, unanswered paused gates across your execution traces pflow resume list # Continue a failed run or answer a paused gate by its token ID pflow resume 7a9e4afb-2095-447e-8043-0660438104f9 --approve yes pflow resume 7a9e4afb-2095-447e-8043-0660438104f9 --choose "deploy-production" ``` Completed upstream steps are restored rather than rerun. The failed step itself starts again, so a partially completed side effect may fire twice; interactive resumes ask for confirmation and non-interactive resumes require `--force`. Resuming by execution ID is independent of the current directory, but the original workflow file must still exist at its recorded path and match the saved content hash. **Highlights** * Added the `pflow resume` CLI command to continue failed or interrupted runs (such as those halted by Ctrl+C or SIGKILL) from the failed node. * Added durable pause state tracking that outputs a resume token and writes paused execution parameters cleanly to the trace file when a gate triggers in non-TTY environments. * Implemented the `pflow resume list` sub-command to scan and display pending unanswered gates awaiting response. * Added direct option mapping for the `--choose` parameter, converting numeric selections or label strings into structured gate resolutions inside the execution loader. * Fixed attempt-chain tracking to prevent dead or interrupted runs from wedging resumption sequences when running `pflow resume` against a workflow path. * Fixed candidate execution scans to ensure only attempts that successfully executed novel work supersede older resumable runs. ## Unified Agent Nodes and AI-Assisted Development We unified our autonomous coding nodes into a single, cohesive execution block. The legacy `claude-code` node has been replaced by a unified `agent` node that supports both Anthropic Claude and OpenAI Codex backends via a clean parameter contract, reducing compilation overhead and aligning schema definitions. ```markdown theme={null} ### refactor-helpers - type: agent - backend: codex - prompt: Modernize standard utility modules in src/core/ - sandbox: workspace-write - approval_policy: never ``` **Highlights** * Introduced the unified `agent` node, allowing workflows to toggle between Claude and Codex behaviors using the `backend: claude|codex` parameter. * Implemented strict parameter-shape checking inside `schema_validation.py` to enforce shared parameters and catch configuration anomalies before runtime. * Improved the `agent` node to raise a structured `PflowError` for parameter validation issues, ensuring failed setups abort early during execution preparation. * Added support for inspecting local workflow files inside `pflow describe`, letting developers review structural parameters without loading or generating execution history. ## Interactive Web UI, Approval Bridges, and Voice Narration The browser visualizer is now a full control center. You can answer paused gates or trigger workflow resumptions directly from the browser canvas. We also launched agent voice narration, bringing stateful point-and-say commentary and automated walkthrough pacing to the UI. ```bash theme={null} # Direct the UI viewer to highlight a node and speak detailed instructions pflow ui focus my-workflow.pflow.md extract-entities --say "We are analyzing this text block." ``` To bypass aggressive browser autoplay blocks that silent walkthroughs encounter, our voice narration engine implements a stateful "hold" loop. CLI pacing automatically pauses execution at blocked narration windows and prompts the user with a playback button in the web UI before continuing. **Highlights** * Added the Web-UI Approval Bridge: rendering interactive Yes/No callout panels on paused nodes and Resume controls on failed nodes inside `pflow ui`. * Added playback beacons (`started`, `blocked`, and `ended` events) that report real-time audio playback state from the browser back to the UI server. * Added Agent Voice Narration, enabling workflows to synthesize voice messages via Gemini TTS using a custom `[bracket]` tag format. * Implemented a closed-loop CLI pacing system (`_await_narration_turn`) that polls the UI server's playback queues to align text narration with physical speech pacing. * Added `pflow settings llm set-tts-model` and `pflow settings llm set-tts-voice` commands to configure global text-to-speech preferences. * Added visual trace indicators to the UI selector, displaying the execution path lineage (`⤷ resumed from `) under resumed attempts. ## Windows Compatibility and Execution Robustness We added Windows support while preserving one portable shell contract: shell steps use POSIX `sh` semantics on every platform, with Git Bash supplying the runtime on Windows. This release also strengthens schema validation, MCP guidance, and trace handling across platforms. ```yaml theme={null} # Local schema validation prevents invalid trace caching type: object properties: validated_output: { type: string } required: [validated_output] ``` **Highlights** * Added Windows compatibility support, establishing Git Bash as the native POSIX shell emulator across win32 environments. * Added Win32-specific pipe detection using Windows file-type handles to support UTF-8 CLI inputs in non-TTY terminals. * Added automatic stdio stream re-encoding, forcing Windows consoles to communicate using byte-exact UTF-8 pathways. * Fixed trace corruption at the LiteLLM integration boundary by deep-copying messages before sending requests to remote providers. * Fixed local LLM schema validation, throwing immediate validation errors and following error-routing paths instead of caching invalid JSON outputs. * Fixed MCP tool output guidance to expose declared nested paths under the canonical `result.*` namespace. * Fixed transient Windows file locks while saving MCP server configuration by retrying atomic replacements. * Fixed the MCP node to strip system sidecar tracking fields (`_source_line` and `_prompt_source_line`) from forwarded tool parameters. ### Claude Code Node Removed The `claude-code` node has been completely removed from the registry. All workflows employing autonomous development agents must migrate to the new `agent` node and declare their target backend: ```markdown Legacy Workflow theme={null} ### edit-utilities - type: claude-code - prompt: Fix the broken unit test. ``` ```markdown Unified Workflow theme={null} ### edit-utilities - type: agent - backend: claude - prompt: Fix the broken unit test. ``` When using the new Codex backend, API-key or custom-provider billing is an explicit opt-in: set `use_api_key: true`. The default fails closed unless Codex has recognized account authentication. [View the full v0.15.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v0150-2026-07-18) ## Interactive Web UI and Live Overlay We launched an interactive visualizer and control center. Using the `pflow ui` command, you can render your entire workflow structure dynamically using an ELK-layout canvas, launch detached execution runs directly from your browser, pre-fill parameters using past-run history, and watch execution progress live via an SSE overlay. ```bash theme={null} # Launch the web-based visualizer and server pflow ui workflow.pflow.md # Switch an active browser viewer to a specific execution run pflow ui workflow.pflow.md --run 3a2c5e6 ``` To protect against remote exploits, our UI server implements loopback-only middleware. This DNS-rebinding guard rejects non-loopback Host headers on both reads and writes, protecting local execution channels. **Highlights** * Added the `pflow ui` command, introducing a web UI using React Flow and ELK layout engines. * Added a live execution overlay that tails execution traces and lights up running, cached, successful, or failed nodes dynamically. * Implemented an interactive execution launcher on the canvas with auto-generated input forms, past-run parameter pre-filling, and detached runner spawning. * Added a stateful agent-to-browser interaction channel ("Point & Watch") with CLI commands `focus`, `frame`, `clear-focus`, and `user-activity`. * Added an output-node panel to inspect resolved inputs, outputs, costs, and token usages per node directly from live execution files. * Fixed multi-tab browser connection limits by releasing connection pooling boundaries for Server-Sent Events (SSE). * Improved UI server robustness with auto-reconnection filters and automatic viewer-tab reuse. * Redesigned UI run selection by git-bucketing histories, grouping runs under local git repositories. ## Declarative Stateful Loops and Human Approval Gates Workflows now support stateful loops and interactive verification steps. You can declare condition-terminated iterations that thread variable states across sequential iterations without side effects, or insert human-in-the-loop steps that pause execution for approval. ```markdown theme={null} ### approve-release - type: agent - backend: claude - prompt: Audit and approve the compiled changelog - approval: required ### iterative-refinement - type: workflow - workflow: ./agent-turn.pflow.md - loop: carry: history: ${iterative-refinement.result} until: ${iterative-refinement.done} max_iterations: 15 ``` **Highlights** * Added human-in-the-loop approval gates via the `approval: required` modifier, pausing execution for interactive YES/NO confirmations. * Added agent-raised escalation gates, allowing `agent` or `code` steps to pause execution using `result.escalation` blocks. * Added the `--auto-approve=` repeatable CLI flag and an equivalent MCP `auto_approve` execution option. * Implemented the `loop:` node modifier with `while:` and `until:` conditions, backed by a robust engine-reentry compiler pattern. * Added the `carry:` state-mapping block (`{ input_key: ${loop-node.output} }`) to thread outputs sequentially across loop iterations. * Added support for settable node-level retry configurations with configurable max attempts, initial wait times, and linear/exponential backoff. * Fixed empty-input batches to output a non-degrading advisory message rather than raising a false `DEGRADED` warning. * Added a loop iteration indicator (`⟳` badge) and self-edge representations to workflow visual layouts. ## Streamable Traces and Content Interning Execution traces are now written incrementally as each node completes, protecting run diagnostics from crash termination. These traces are compressed automatically by interning string blobs at the file boundary and canonicalizing LLM-producing node parameters. ```bash theme={null} # Re-run a specific node using its frozen execution snapshot pflow workflow.pflow.md --only extract-entities ``` Because streamable traces use raw JSONL entries, pflow can read incomplete runs. If a run crashes, the system can reconstruct trace logs up to the failure point while gracefully dropping dangling sub-workflow nodes. **Highlights** * Rebuilt tracing as an incremental JSONL streaming transport, flushing execution events to disk line-by-line during runtime. * Added content interning at the disk boundary, replacing the legacy `blobs` trailer with inline-first-occurrence blob keys. * Shrink trace footprint by canonicalizing LLM prompts and systems, stripping redundant template resolutions and raw parameters. * Added prewarm-batch prompt optimization, capturing shared prefixes as cache blocks to prevent duplicate prompt storage. * Fixed the `--only` execution flag to restore a frozen snapshot of historical upstream inputs instead of re-firing side-effecting steps. * Added support for reporting true input token counts and agent execution calls in output summaries. * Added cache-aware API-equivalent cost estimates for Codex agent calls when workflows declare a LiteLLM-priced `model`. * Added best-effort trace writing, allowing runs to execute safely even under disk-full or read-only filesystem environments. ## LLM Control and Execution Refinements We adjusted default execution settings, upgraded the `??` coalesce operator to support JSON literals, and added type coercion capabilities to Claude Code output schemas. ```markdown theme={null} - prompt: Process this data: ${read.content ?? "empty fallback"} - prompt: Score threshold: ${assess.score ?? 0.8} ``` **Highlights** * Flipped memoization defaults: only `llm` nodes cache by default; all other node types require explicit `- cache: true`. * Added support for JSON literals (`string`, `number`, `boolean`, `null`) inside `??` template coalesce operations. * Improved the `??` operator to fall through on absent sub-fields and unexecuted branches without raising validation errors. * Added self-healing schemas to `agent` outputs, enabling scalar type coercion and resume-retries on schema soft-failures. * Added pre-execution validation for LLM model IDs, throwing validation errors before initiating live provider calls. * Unified reasoning budget parameter mappings (`reasoning_effort` and `reasoning_max_tokens`) across LLM providers. * Added support for shielding `${...}` templates in flow-style YAML, preventing unquoted inline maps from parsing incorrectly. * Fixed `code-node` type validations, allowing outputs of type `list[T]` to satisfy parameters expecting `list[str]`. * Added a redirection helper for CLI users typing `pflow validate ` or `pflow check `, suggesting the correct `--validate-only` command. * Added calendar validation for task scripts, rejecting impossible dates like leap-year boundary failures. ### Caching Defaults Flipped Non-LLM nodes (such as `code`, `shell`, and `http`) no longer cache execution results by default. Workflows relying on memoization for these steps must explicitly set `- cache: true`. ### Mermaid Command Renamed The `pflow visualize` command has been renamed to `pflow mermaid`. ### Storage Mode Removed The `storage_mode` parameter has been deleted entirely. ### Agent API-key opt-in `use_api_key` is now one shared strict permission on `agent` nodes. In false/default mode, Claude blanks `ANTHROPIC_API_KEY`; Codex removes `OPENAI_API_KEY` / `CODEX_API_KEY`, requires recognized account auth from `codex login status`, and pins the OpenAI provider selector. Existing Codex workflows that intentionally use API-key or configured-provider billing must add `use_api_key: true` or they now fail before the model call. This guard controls the named key and ordinary provider paths; account credits, auto-reload, overage, custom proxies/base URLs, and administrator policy remain provider/user controls. ### MCP Result Access MCP nodes no longer copy top-level result fields into `${node.field}` or create a `{server}_{tool}_result` alias. Access tool output through the canonical `${node.result.field}` path instead. [View the full v0.14.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v0140-2026-07-03) ## Declarative prompt caching and `pflow analyze-cache` We introduced provider-level prompt caching as a declarative workflow surface. By defining a top-level `## Cache` section and declaring chunks on your LLM nodes, prompt prefixes are cached directly at the provider level (such as Anthropic and Gemini), dramatically reducing costs and latency for repetitive system instructions and context. ````markdown theme={null} ## Cache - ttl: 5m ```cache [brief] The project brief we are working from: ${brief} ``` ### extract-features - type: llm - prompt_cache: [brief] - prompt: Extract features from ${input.text} ```` To help discover caching opportunities, the new `pflow analyze-cache` command performs static and trace-driven analysis of your workflow. It estimates cacheable tokens, models per-call cost projections, and provides actionable, headline-led recommendations without executing live model calls. ```bash theme={null} # Analyze cache efficiency and get optimizations from a previous execution trace pflow analyze-cache my-workflow.pflow.md --from-trace ``` Provider minimums apply for prompt caching (e.g., 1024 tokens for Anthropic Claude 4.5 Sonnet, 4096 tokens for Gemini pro models and 2048 for flash models). `pflow analyze-cache` automatically matches your prompt sizes against the target model's specific thresholds to prevent you from writing cache blocks that are too small to activate. **Highlights** * Added the top-level `## Cache` markdown section to parse declarative, reusable prompt chunks. * Added the `prompt_cache` and `prewarm` properties to LLM nodes to control cache inclusion and gate automatic batch-prefix caching. * Implemented the `pflow analyze-cache` CLI command (and matching MCP tool) with Greenfield, Steady-state, Trace-driven, and Already-optimal analysis modes. * Added a 22-entry warning catalog to flag cache misalignments, prompt shadow duplicates, and missing shared references. * Implemented dynamic cache TTL translation for Gemini models (`cachedContents`) alongside Anthropic native controls. * Added support for multi-breakpoint prompt caching for Anthropic models. * Implemented synthetic cache warmup capabilities and detailed cost telemetry in trace formats. * Fixed prewarm diagnostics to correctly account for declared `prompt_cache` chunks. ## Native structured outputs for Claude Code We replaced Claude Code's prompt-injected and regex-extracted structured output system with native JSON Schema support using `claude_agent_sdk`'s native structured output capabilities. This ensures guaranteed schema compliance without competing system instructions or fragile string parsing. ````markdown theme={null} ### audit-code - type: agent - backend: claude - max_turns: 5 - prompt: Audit the security of the root directory. ```yaml output_schema type: object properties: vulnerabilities: type: array items: type: string required: [vulnerabilities] ``` ```` Claude Code structured output failures are now treated as "soft failures." If the session fails to comply with the schema, the raw text is preserved, a `_schema_error` is set, and the run continues in a `DEGRADED` status rather than raising a hard error that aborts your workflow. **Highlights** * Upgraded the Claude Code node to use `ResultMessage.structured_output` via the native SDK, requiring a minimum of `claude-agent-sdk>=0.2.82`. * Added preflight and static validation to enforce that Claude Code output schemas define a top-level `type: object`. * Added a strict constraint requiring `max_turns >= 2` when an output schema is defined to allow planning and output turns. * Implemented warning rehydration inside the persistent memoization cache, ensuring that cached soft-failures correctly replay as `DEGRADED` rather than passing as successful. * Narrowed exception handling within Claude Code sessions to `ProcessError` to allow actionable SDK remediation messages (like installation and doctor checks) to bubble up. * Aligned the Claude node's output parsing by linking the "Outputs" parse-hint with the accepted `source` key, and stripped runtime-internal vocabulary from agent error messages. ## LiteLLM integration and offline pricing We rebuilt our LLM execution layer, replacing the existing integration with a pflow-owned adapter backed natively by LiteLLM. This migration unifies over 100 model providers under a single, robust seam with unified exception mapping and cost tracking. ```bash theme={null} # Discover environment variables required for your configured LLM models pflow settings llm providers ``` **Highlights** * Replaced the `llm` package with a lazy-imported LiteLLM wrapper, improving overall CLI startup performance. * Added a typed exception hierarchy under `LLMCallError` to provide structured discriminators (`UnknownModelError`, `MissingApiKeyError`, `LLMTransientError`) for downstream retry and fallback routing. * Redesigned tracing to use a `shared["__trace_collector__"]` save/restore context, fixing a thread-boundary bug where literal prompt captures were silently omitted. * Added a deterministic offline pricing map for LiteLLM models to ensure accurate cost estimation when pricing metadata is missing from local snapshots. * Added automatic bare model name prefixing (e.g., `gpt-4o` normalized to `openai/gpt-4o`) to ensure correct routing. * Fixed a pricing bug where `cost_usd` was left unpopulated for newer LLM models released after our bundled LiteLLM snapshot. * Upgraded the default LiteLLM engine version and fixed `reasoning_effort` mapping for Anthropic Claude 3 Opus. ## CLI usability and parser refinement We refined our CLI output, added more helpful diagnostics to failing pipelines, and improved the core Markdown parser to handle complex YAML block structures. **Highlights** * Added support for dotted paths inside CLI output destination flags (e.g. `-o data.nested.field`). * Added walk-to-failure hints to the terminal output on workflow failures to assist agents in debugging sequence halts. * Implemented a more compact summary representation for large batch node executions. * Fixed linting for shell nodes without template inputs to offer both available cache resolutions in the warning text. * Fixed the markdown parser to preserve blank lines inside multi-line YAML block scalars, preventing layout corruption of templates and prompts. ### Claude Code Python-alias schemas removed Legacy Python-alias type names (`str`, `int`, `list`, `dict`) are no longer supported inside Claude Code `output_schema` blocks. All schemas must use standard JSON Schema types and declare a top-level `type: object`. ```yaml Legacy Schema (Deprecated) theme={null} risk_level: str issues: list ``` ```yaml JSON Schema (Required) theme={null} type: object properties: risk_level: type: string issues: type: array items: type: string required: [risk_level, issues] ``` ### Claude Code Turn minimums Workflows declaring a Claude Code node with an `output_schema` must set `max_turns` to `2` or greater. Workflows specifying `max_turns: 1` will fail validation. ### AdapterResponse shape changes The internal response object from LLM calls no longer carries `error` or `status` fields. Failure states now consistently raise a subclass of `LLMCallError`. Downstream steps catching custom exceptions should catch `LLMCallError` and read structured attributes (`kind`, `reason`). ### LiteLLM package pinning and model names * Simon Willison's `llm` CLI and its associated provider plugins are no longer used. Environment variables (like `OPENAI_API_KEY`) are read directly from the shell or via `pflow settings`. * LLM nodes require provider-prefixed model names (e.g. `openai/gpt-4o` instead of `gpt-4o`). Bare names are auto-prefixed based on common naming conventions, but unknown bare names will pass through unchanged. ### Caching flag behavior The `--no-cache` CLI flag now only bypasses local pflow memoization reads. It does not disable LLM provider-level prompt caching. [View the full v0.13.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v0130-2026-05-26) ## Execution previews and planning We introduced a high-fidelity execution planner via the `--dry-run` flag. It provides historical LLM cost and duration estimates without invoking side effects, using the same cache-key and template-resolution logic as the live engine to ensure zero drift. ```bash theme={null} # Preview execution with cost and duration estimates pflow workflow.pflow.md --dry-run ``` The planner now supports full per-item recursion for batch sub-workflows, allowing you to see exactly which items in a large parallel run would execute and which would serve from cache. **Highlights** * Added the `--dry-run` flag to provide execution plans with historical cost (\$USD) and duration estimates. * Implemented recursive batch planning that aggregates child summaries into a single synthetic plan, correctly handling parallel vs. sequential durations. * Added a `cost_basis` indicator (`upper_bound` vs `exact`) to dry-run summaries to help agents gate high-cost operations. * Fixed dry-run recursion for batch sub-workflows to prevent false validation errors on `${item}`-backed child inputs. ## Modernized CLI and guidance The CLI has been flattened into a focused, top-level surface, and the monolithic agent instructions have been replaced by `pflow guide`—a topic-scoped system that delivers framework and node-specific guidance at runtime. ```bash theme={null} # Access topic-scoped guidance for agents or users pflow guide llm code batch ``` **Highlights** * Flattened the CLI surface: `pflow workflow ` and `pflow registry ` are now top-level commands like `pflow list`, `pflow describe`, and `pflow probe`. * Added `pflow guide`, a content delivery system that composes help topics based on requested scope or auto-detected workflow features. * Improved non-interactive output routing: live progress now streams to `stderr` in real-time, while node data is routed strictly to `stdout` for clean piping to `jq`. * Added an exception boundary to the MCP server via a `FastMCP` subclass to provide structured diagnostics to agents. * Consolidated `pflow trace report` into the flattened `pflow report` command. * Added confidence-based guidance and runnable command hints to the `pflow find` output. ## Type safety and native diagnostics We refactored the workflow type vocabulary to use canonical JSON Schema names and moved the validation engine to produce structured `Diagnostic` objects natively. This ensures that typos and contract violations are caught early with rich, actionable "Did you mean?" suggestions. The new type vocabulary is strictly enforced at the sub-workflow boundary. Any value crossing from parent to child must be declared on the child workflow, or it will be rejected at parse time. **Highlights** * Refactored the type vocabulary to 7 canonical names: `string`, `integer`, `number`, `boolean`, `array`, `object`, and `any`. * Upgraded the validator to produce `Diagnostic` objects natively, enabling numbered lists, fuzzy suggestions, and source-line tracking in all validation errors. * Implemented a strict parent-to-child input boundary for sub-workflows that rejects undeclared inputs at both parse-time and runtime. * Added validate-time type checking for Python code-node input and result/next annotations. * Unified the `??` coalesce operator to correctly distinguish between nodes that were skipped and nodes that failed. * Fixed workflow and template validators to short-circuit on structural errors, preventing cascades of redundant messages. ## Rich Mermaid visualization The Mermaid visualization system has been transformed into a full data-flow engine. It now renders sub-workflow boundaries, external IO subgraphs, and data-provenance edges derived from template references. ```bash theme={null} # Visualize data-flow and batch semantics in Mermaid format pflow visualize my-workflow.pflow.md --descriptions ``` **Highlights** * Added support for "External IO" wrappers that render sub-workflow inputs and outputs as dashed subgraphs outside the primary pipeline. * Added data-flow edges that trace the actual provenance of data through `${node.field}` template references. * Implemented the Mermaid `procs` shape (stacked rectangles) to visually communicate batch parallelism. * Added `--descriptions` support to the visualizer to include node-level documentation in the rendered output. * Improved layout logic to connect top-level inputs directly to their nearest consumer, preventing long-range edges from distorting the diagram. ## Reliability and invariants This release addresses several critical execution invariants, particularly how the shared store handles node failures and how batch results are filtered. **Highlights** * Fixed the failed-node invariant: data from failed nodes now moves to `shared["__failures__"]` instead of leaking into `shared[node_id]`, preventing downstream nodes from accidentally reading partial or failed results. * Updated batch processing to exclude failed items from the `results` array, ensuring downstream nodes receive only successful data. * Added protection against `__dunder__` parameter names to prevent workflows from accidentally overwriting internal framework state. * Implemented a `_ProgressPartialLineFilter` to prevent `logger.warning` messages from corrupting live progress lines. * Added automatic node registry refreshing when source files change on disk. * Fixed on-error recovery reporting to correctly reflect a `DEGRADED` status instead of a false `SUCCESS`. ### Flattened CLI commands The hierarchical `workflow` and `registry` namespaces have been removed. * `pflow workflow save` → `pflow save` * `pflow workflow history` → `pflow history` * `pflow registry run` → `pflow probe` * `pflow instructions` → `pflow guide` * `pflow mcp tools` → `pflow mcp list` ### Type vocabulary refactor Python type aliases (`str`, `int`, `list`, `dict`) are no longer supported in `## Inputs` or `## Outputs`. * Use `string` instead of `str`. * Use `integer` instead of `int`. * Use `array` instead of `list`. * Use `object` instead of `dict`. * Use `any` for wildcards. ### Sub-workflow input strictness * The `workflow_ir` inline-IR escape hatch has been removed. Use file references instead. * Sub-workflows now reject undeclared inputs. Any parameter passed from a parent must be explicitly declared in the child's `## Inputs` section. ### Failed-node data location Data from failed nodes is no longer available at `shared[node_id]`. It is archived in `shared["__failures__"][node_id]`. Standard template references to failed nodes will now correctly trigger "node did not execute" errors unless the coalesce operator (`??`) is used. ### Batch result filtering When `error_handling: continue` is used, the `results` list in the batch output no longer contains `None` or error objects for failed items; it only contains successful results. [View the full v0.12.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v0120-2026-04-22) ## Execution core and iteration speed We rebuilt the execution core with a standalone orchestration engine that compiles workflows once per batch, dropping compilation overhead to near zero. A new SQLite-backed memoization cache lets you iterate rapidly by re-using results for unchanged nodes. ```bash theme={null} # Re-run a workflow, executing only 'target-node' and its dependencies pflow my-workflow.pflow.md --only target-node ``` The memoization cache is automatically propagated to all nesting levels, meaning unchanged sub-workflows and nodes safely serve cached results without re-executing. **Highlights** * Redesigned the execution core to compile sub-workflows once per batch, yielding a \~7x speedup for large parallel iterations. * Added a persistent memoization cache system with `--only` and `--no-cache` CLI flags for precise, rapid iteration control. * Added per-node cache opt-out support via the `cache: false` property. * Fixed concurrent mutation issues in the workflow executor during compilation. * Fixed an intermittent bug where zombie threads caused stream corruption (`I/O operation on closed file`) in Python code nodes. ## Execution reports and traces The trace system has been redesigned from flat JSON snapshots to a tree-structured format. You can now generate navigable Markdown reports that display exact rendered prompts, outputs, and LLM costs for every node. ```bash theme={null} # Generate a directory of Markdown execution reports pflow my-workflow.pflow.md --report-dir ./report/ ``` Because execution reports are saved as standard directories of Markdown files, you can use `git diff report/` to easily compare prompt renders and outputs between workflow runs. **Highlights** * Added the `--report` flag to generate a directory of Markdown files detailing execution, including input/output token breakdowns and rendered templates. * Upgraded trace format to 2.0.0: uses tree-structured events that correctly nest batch items and sub-workflows without truncating data. * Added smart anomaly detection in reports to flag empty outputs (like dropped HTTP bodies) and provide template fix suggestions. * Cross-cutting infrastructure keys and LLM costs now propagate correctly through deeply nested sub-workflows. * Added LLM parameters (temperature, reasoning effort, system prompt) to node metadata in reports. * Fixed cache invalidation for sub-workflow changes and eliminated phantom cost reporting in traces. ## File references and workflow bundling Code block parameters can now reference external files directly. To support this safely, `pflow workflow save` now bundles workflows and their file dependencies into self-contained directories. ```markdown theme={null} ### analyze-code - type: llm - prompt: ./prompts/code-review.md ``` **Highlights** * Code-block parameters (`prompt`, `code`, `command`, `batch`, `output_schema`) can now reference external files. The system auto-detects paths, reads the files, and resolves templates inside them at compile time. * `pflow workflow save` now packages workflows as folders containing the entry point and all referenced dependencies, preserving relative directory structures. * Added per-item parameter overrides in batch nodes — each item can set its own `model`, `reasoning_effort`, or any other node parameter, so a single batch can mix providers or effort levels. * Added the `inputs` parameter as template context for all node types, not just code nodes. * Fixed relative path resolution so sub-workflow dependencies always resolve against the sub-workflow's directory, not the current working directory. * File references in sub-workflows are now fully resolved before validation. ## Validation and diagnostics We rebuilt the CLI error output pipeline and validation engine. Every error now renders in a single diagnostic format — title, location, context block, and fix suggestion (e.g., "Did you mean 'file\_path'?") — consistent across text and JSON output. The parser catches deeper structural issues before execution begins. ```bash theme={null} # Visualize workflow structure using Mermaid pflow visualize workflow.pflow.md --direction TD ``` **Highlights** * Added the `pflow visualize` command to generate Mermaid flowcharts of workflow topologies. * Unified diagnostic rendering into a single, structured format with self-describing exceptions for both CLI text and JSON outputs. * Added recursive sub-workflow validation at parse time to catch structural errors, unknown node types, and missing required inputs before execution. * Added a 120-second default timeout to LLM nodes to prevent workflows from hanging indefinitely on stalled API calls. * LLM nodes now catch `JSONDecodeError` when `output_schema` fails, preserving the raw text in the response and returning a soft error. * Improved the markdown parser to detect orphaned content, duplicate section headings, and provide actionable errors for unquoted colons in YAML parameters. * Fixed a bug where type annotations in Python code YAML inputs (e.g., `text: str = ${ref}`) were incorrectly parsed as literal values. * Unified output auto-detection across CLI, JSON, and MCP interfaces. * Added a CLI hint suggesting `pflow mcp sync` when a registry search returns no results but matching MCP servers exist. * Fixed an issue where the registry cache would not refresh after a package upgrade. * Fixed issues with template deduplication, double validation, raw string parsing in single-line YAML, and bash syntax correctness. * Fixed missing error details in JSON output, dead code in error display paths, and redundant template resolution. ## Under the hood This release includes a major structural overhaul: 18,000 lines of production code removed across 16 refactoring PRs. The PocketFlow framework was replaced with a standalone orchestration engine, the exception hierarchy was consolidated under a single base class, and the CLI, compiler, and runtime were each decomposed into focused modules. These changes don't add features directly — they're what made the compile-once optimization, unified diagnostics, and shared execution pipeline possible. ### Workflow save format Workflows are now saved as folders (`~/.pflow/workflows/{name}/{name}.pflow.md`), not flat files. This change enables proper file dependency bundling but will break existing symlinks for published skills. ### Unknown parameters block execution Unknown parameter detection was promoted from non-blocking warnings to blocking validation errors to catch typos early. Typos like `- path:` instead of `- file_path:` will now prevent execution. ### Unified JSON error shape All error paths in the CLI now produce a single, unified JSON shape. ```json Before theme={null} { "success": false, "is_error": true, "failed_node": "fetch" } ``` ```json After theme={null} { "success": false, "status": "failed", "error": "Execution failed", "errors": [{"message": "...", "category": "..."}] } ``` ### Batch error handling * Batch nodes now abort with a `RuntimeError` when all items fail and `error_handling: continue` is set, rather than returning garbage data. * Batch nodes no longer swallow compilation errors under `continue` mode. * When `continue` mode recovers from partial failures, it now returns a `"default"` action and sets a `DEGRADED` status instead of returning an `"error"` action that halts the workflow. * Sub-workflow error actions and permissive batch template errors are now properly propagated and detected in batch processing. ### Removal of internal nodes The experimental `git`, `github`, `test`, and `echo` nodes have been completely removed from the registry and documentation. ### Workflow termination The engine now recognizes `"end"` and error-only successors as intentional workflow terminations rather than validation failures. ### Trace format and LLM calls The parallel `__llm_calls__` accumulator was removed. Trace events are now the single source of truth for LLM costs. Trace format has been bumped to 2.0.0, removing all value truncation and replacing full-store snapshots (`shared_before`/`shared_after`) with focused per-node parameters. [View the full v0.11.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v0110-2026-04-05) ## Branch convergence Workflows now support branch convergence. When downstream nodes need to reference "whichever branch ran" after a conditional split, you can use the new coalesce operator (`??`) or optional inputs in code nodes to handle skipped branches. ```markdown theme={null} ### summarize - type: llm - prompt: Result was: ${branch-high.stdout ?? branch-low.stdout} ``` The coalesce operator checks if the root node actually executed. If `branch-high` was skipped, it falls through to `branch-low`. If the node *did* execute but you misspelled the field (e.g., `stddout`), it catches the typo and raises an error instead of silently falling through. **Highlights** * Added the `??` coalesce operator for template syntax (`${a.stdout ?? b.stdout}`). The resolver tries each operand left-to-right and skips branches that did not execute. * Coalesce is supported in all template contexts: inline strings, shell commands, LLM prompts, input dicts, workflow output sources, and batch items. * Python code nodes now accept `Optional[T]` or `T | None` input annotations. If the source branch didn't execute, `None` is injected automatically instead of raising a runtime error. ## Nested workflows Workflow nodes now look and behave like every other node type. You pass parameters as regular inputs, and child outputs are exposed via the standard namespace system. ```markdown theme={null} ### process-document - type: workflow - workflow: ./child.pflow.md - title: "Hello World" - body: ${read-file.content} ``` Child workflow inputs are validated before execution starts. If you miss a required input or provide the wrong parameter name, the parent workflow fails immediately with an error listing the child's declared inputs. **Highlights** * Unified `workflow` parameter handles both file paths and saved workflow names. * Non-reserved parameters become child inputs. * If the child workflow declares `## Outputs`, they are exposed to the parent via standard dot notation (`${node_id.output_name}`). * Fixed relative path resolution so `./child.pflow.md` always resolves from the parent workflow's directory, even across deep nesting levels. * The template validator now statically resolves child workflow outputs to catch typos during compilation. ## LLM reasoning and cost tracking You can now control reasoning and thinking depth across all LLM providers using a unified interface. pflow translates your settings to the provider-specific parameters — Anthropic's `thinking_budget`, OpenAI's `reasoning_effort`, Gemini's thinking config. ```markdown theme={null} ### analyze-data - type: llm - model: claude-sonnet-4-5 - reasoning_effort: high - prompt: Analyze this dataset... ``` **Highlights** * Added `reasoning_effort` (xhigh, high, medium, low, minimal, none) and `reasoning_max_tokens` (direct token budget) to the LLM node. * Added a `model_options` parameter to the LLM node as an escape hatch for provider-specific fields. * Unified LLM cost access: `${node.llm_usage.cost_usd}` now works in workflow templates for both standard LLM and Claude Code nodes. * LLM costs are computed at execution time, making `cost_usd` available in the shared store immediately after each node runs. * The Claude Code node's redundant `_claude_metadata` output was removed; all metadata is consolidated into `llm_usage`. ## Validation and execution robustness Several compile-time and runtime edge cases around batch processing, validation depth, and parallel execution have been fixed. **Highlights** * The template validator now infers the internal structure of batch items based on the upstream batch source, catching invalid `${item.field}` references at compile time with "did you mean?" suggestions. * Validation now recurses into nested dictionaries and lists (such as the `inputs` dict on code nodes) to catch typos and non-existent forward references. * Required workflow inputs now strictly fail validation when provided as empty strings. * Batch nodes now return an "error" action on partial failures when `error_handling: continue` is set, enabling proper `on-error` routing for partial batch failures. * Fixed a bug where the validator blocked batch processing entirely on nested workflow nodes. * Resolved a `_thread.RLock` pickle error that caused parallel batch processing to crash at runtime. **Other** * Added `--timeout` and `--sse-timeout` flags to `pflow mcp add` so custom timeout values are preserved in config files. ### Removal of planning module and repair system The built-in natural language planning module and auto-repair systems have been removed (\~40,000 lines of code). AI agents handle planning directly — pflow provides the runtime, validation, and execution primitives they compose. * Removed CLI flags: `--trace-planner`, `--planner-timeout`, `--planner-model`, `--auto-repair`, `--cache-planner`, `--save/--no-save`, `--no-update`, and `--generate-metadata`. * Component and workflow discovery features have been preserved as plain functions for agents to query available resources. ### Nested workflow API * The `workflow_ref` and `workflow_name` parameters have been consolidated into a single `workflow` parameter. * `param_mapping` and `output_mapping` have been removed entirely. Pass arguments directly as inputs, and access outputs via standard dot notation. * `isolated` and `scoped` storage modes have been removed. ```yaml Before (v0.9.0) theme={null} ### process - type: workflow - workflow_ref: ./child.pflow.md - param_mapping: text: ${source.text} - output_mapping: summary: child_summary ``` ```yaml After (v0.10.0) theme={null} ### process - type: workflow - workflow: ./child.pflow.md - text: ${source.text} # Outputs are automatically mapped to ${process.summary} ``` ### Unresolvable output errors When a workflow output source references a node that didn't execute (e.g., a branch not taken) and no `??` coalesce operator is used, it now raises an `OutputResolutionError` with a precise diagnostic message. Previously, these unresolvable outputs were silently dropped, causing confusing downstream failures in nested workflows. ### Removed registry run timeout flag The `--timeout` flag on the `pflow registry run` command has been removed as it was redundant. Timeouts can be passed directly as a per-node parameter (e.g., `timeout=30`). [View the full v0.10.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v0100-2026-03-17) ## Conditional branching and loops Workflows now support conditional routing. You can branch based on errors, make data-driven routing decisions in Python code nodes, and create retry loops directly in your `.pflow.md` files. ````markdown theme={null} ### router - type: code - on-error: error-handler ```python code data: dict if data["category"] == "premium": next: str = "premium-handler" else: next: str = "standard-handler" ``` ```` To prevent infinite loops, an automatic loop guard tracks node visits and raises a `MaxNodeVisitsError` if a single node is executed 100 times (configurable via `PFLOW_MAX_NODE_VISITS`). **Highlights** * Added `- next:`, `- on-error:`, and `- next: end` syntax for static and error routing. * Python code nodes support a `next` variable for dynamic, data-driven routing. * Caching is now automatically invalidated when a node is revisited in a loop, ensuring exit conditions are correctly re-evaluated. * `flow.run()` is now always wrapped to ensure visit counts reset correctly between executions. * Topological sorting now uses position-based edge filtering to allow valid data dependencies while preventing cycle errors on backward loop edges. ## Guaranteed structured JSON The LLM node now accepts an `output_schema` parameter for guaranteed structured JSON responses. It uses the constrained decoding APIs of model providers (Anthropic, Gemini, OpenAI) instead of prompting for JSON and hoping the model complies. ````markdown theme={null} ### extract-entities - type: llm - prompt: Extract entities from ${read.content} ```yaml output_schema type: object properties: people: type: array items: type: string required: - people ``` ```` When `output_schema` is provided, the node parses the response directly into a dictionary. Downstream nodes can access fields immediately via `${extract-entities.response.people}` without an intermediate extraction step. **Highlights** * `yaml output_schema` code blocks pass JSON Schema dicts directly to the `llm` library. * The API response is parsed and stored as a `dict`, avoiding downstream string parsing. * Code block stripping is safely skipped when a schema is set, since the API returns clean JSON. ## Stateful MCP servers and error reporting MCP servers are no longer restarted for every node in a workflow. A background event loop now acts as a connection pool, keeping server sessions alive across workflow steps. This fixes silent failures where stateful servers (like Playwright browsers or database clients) lost all state between step executions. **Highlights** * Persistent connection pool keeps both `stdio` and `http` MCP sessions alive across nodes. * Automatic crash recovery evicts and retries sessions once if a transport error (like a broken pipe) occurs. * MCP error reporting now un-wraps internal `ExceptionGroup` task failures to show the actual HTTP error (e.g., "Authentication failed" instead of a raw 40-line traceback). * Fixed a logging bug that prepended "MCP tool failed:" twice. * Node output formatter correctly detects `error` keys so MCP failures show as "failed" instead of "succeeded". ### Explicit branch target routing Nodes reached via explicit routing (branch targets) used to silently fall through to the next node in document order. This silent, input-dependent bug has been fixed via parse-time validation. Any node targeted by an action edge must now explicitly declare its next step. ```yaml Before (v0.8.0) theme={null} ### router - type: code - on-error: error-handler ### error-handler - type: shell # Silently fell through to the next node below it ``` ```yaml After (v0.9.0) theme={null} ### router - type: code - on-error: error-handler ### error-handler - type: shell - next: end # Explicit routing now required ``` ### Dynamic routing validation If a Python code node assigns a variable to `next` (e.g., `next = target_var` instead of a literal `"node-id"`), you must explicitly declare `- next:` in the markdown step parameters so the parser can build the execution graph. ### ReadFile outputs raw content The `ReadFile` node previously prepended line numbers (`N: `) unconditionally to every line it read. This corrupted file content for downstream LLM prompts, templates, and configurations. It has been completely removed; the node now returns raw, unmodified file content. ### Python code node results The `result` output variable is now optional in Python code nodes as long as `next` is declared. [View the full v0.9.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v090-2026-03-14) First public release on PyPI. pflow is a CLI workflow engine — AI agents write `.pflow.md` files that chain shell commands, LLM calls, HTTP requests, and Python code through a shared data store. Workflows run the same way every time, without burning tokens on repeated tool calls. ```bash theme={null} uv tool install pflow-cli ``` ## Agent skills You can now publish workflows as native skills for AI agents. The `pflow skill` command symlinks your saved workflows to the configuration directories for Claude Code, Cursor, GitHub Copilot, and Codex. ```bash theme={null} # Publish a workflow to multiple tools at once pflow skill save pr-analyzer --cursor --copilot ``` Published skills are symlinks, not copies. When you edit the original workflow, the agent's skill updates instantly without needing to re-publish. **Highlights** * `pflow skill save` enriches workflows with usage sections and metadata for the agent. * Support for multiple targets: `--cursor`, `--copilot`, `--codex`, and `--personal`. * `pflow workflow history` shows execution stats and last-used inputs. * Improved discovery matching by including input names and node IDs in the context. ## Data integrity LLM nodes no longer discard prose when extracting JSON. Previously, if a response contained a JSON block, the node threw away the surrounding text. Now, the full response is stored as a string, and JSON parsing happens on-demand via the template system. **Highlights** * LLM nodes preserve prose explanations alongside code blocks. * JSON fields are still accessible via dot notation: `${node.response.field}`. * Numeric strings (like Discord IDs) declared as `type: string` are no longer coerced to integers. * Batch node error messages now correctly list available outputs for inner items. * Workflow frontmatter tracks average execution duration for performance monitoring. ## Developer experience Runtime errors in Code nodes now point to the exact line number in your `.pflow.md` file, rather than the temporary Python script. We also improved environment variable handling in MCP configurations to support dynamic URLs. **Highlights** * Code node errors show `Location` and `Source` fields with correct line mapping. * MCP server configs now expand environment variables in URLs and `settings.json`. * Markdown parser specifically detects and explains nested backtick errors. The PyPI package is `pflow-cli`, not `pflow` (that name was already taken). This is the first PyPI release — if you installed from git before, switch to: ```bash theme={null} uv tool install pflow-cli # or pipx install pflow-cli ``` [View the full v0.8.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v080-2026-02-10) ## Workflows are documentation Workflows have moved from JSON to a custom Markdown format (`.pflow.md`). The file *is* the documentation — H1 headers become titles, prose becomes descriptions, and code blocks define execution logic. Comments and formatting are preserved when saving, so your notes survive round-trips through the CLI. ```markdown theme={null} # Daily Report ## Steps ### fetch-data - type: http - url: https://api.github.com/repos/owner/repo/issues ### summarize - type: llm - prompt: Summarize these issues: ${fetch-data.response} ``` The internal parser produces the exact same IR structure as before, so execution logic is unchanged. The migration is purely about authoring experience and LLM readability. **Highlights** * New `.pflow.md` extension with YAML frontmatter for metadata. * Line-by-line error reporting with context, replacing JSON syntax errors. * "Save" operations update the file in place, preserving your comments. * `pflow workflow save` extracts the description directly from the document prose. ## Native Python execution The new `code` node runs Python in-process, passing native objects (lists, dicts) between steps without serialization overhead. Unlike the shell node, it doesn't need `jq` to parse inputs — `inputs` are injected directly as local variables. ````markdown theme={null} ### transform-data - type: code - inputs: data: ${fetch.response} limit: 10 ```python code data: list limit: int result: list = data[:limit] ``` ```` **Highlights** * Zero-overhead data passing for heavy transformations. * Required type annotations catch type mismatches before execution. * `stdout`/`stderr` capture for debugging, with configurable timeouts. ## Unix piping and validation You can now chain workflows using standard Unix pipes. Mark an input with `stdin: true`, and pflow will route piped data to that specific parameter. Validation has also been unified: the checks that run during `--validate-only` now run before every execution, catching errors like invalid JSON string templates before any steps run. ```bash theme={null} # Chain workflows with the -p (pipe) flag pflow -p fetch-logs | pflow -p parse-logs | pflow analyze-errors ``` The validator now detects the "JSON string with template" anti-pattern (e.g., `"body": "{\"val\": \"${var}\"}"`) and suggests the correct object syntax to prevent runtime JSON errors. **Highlights** * `stdin: true` input property for explicit pipe routing. * FIFO detection prevents hangs when no input is piped. * Unified validation logic ensures `--validate-only` matches runtime behavior. * Improved error messages for unknown node types (no more stack traces). * `disallowed_tools` parameter on Claude Code nodes to block specific tools in agentic workflows. * Fixed nested template validation for `${item.field}` inside array brackets. ### Workflow format JSON workflow files (`.json`) are no longer supported. Existing workflows must be converted to the `.pflow.md` format. The CLI will reject JSON files with a migration error. ### Stdin handling The `${stdin}` shared store variable has been removed. You must now explicitly mark an input parameter to receive piped data. ```yaml Before theme={null} ### process - type: shell - stdin: ${stdin} ``` ```yaml After theme={null} ### input_name - type: string - stdin: true # Routes pipe here ``` ### CLI changes * `pflow workflow save` no longer accepts `--description`. It extracts the description from the Markdown content (text after the H1 header). * Metadata is now stored in YAML frontmatter rather than a `rich_metadata` wrapper. [View the full v0.7.0 changelog with PR references on GitHub →](https://github.com/spinje/pflow/blob/main/CHANGELOG.md#v070-2026-02-04) ## Batch processing Need to classify 50 commits with an LLM, or fetch 200 URLs? Add a `batch` config to any node and pflow handles the fan-out. Works with every node type — LLM, shell, HTTP, MCP, all of them. ```yaml theme={null} ### classify-items Classify each item in parallel. 30 concurrent LLM calls. - type: llm - batch: items: ${fetch-data.result} parallel: true max_concurrent: 30 ``` Results stay in input order even in parallel mode, and each result includes the original item — so downstream nodes can always correlate outputs back to inputs without extra bookkeeping. **Highlights** * Sequential and parallel execution with configurable concurrency (`max_concurrent`). * `error_handling: continue` keeps going when individual items fail — you get partial results instead of nothing. * Progress indicators in the CLI so you can see where a 200-item batch is at. * Access results with `${node.results}`, individual items with `${node.results[0].response}`. ## Smarter templates Template variables like `${node.stdout.items[0].name}` now parse JSON automatically. If a shell command outputs a JSON string, you can access nested fields directly — no more `jq` extraction steps between every shell node and the thing that consumes it. **Highlights** * `${node.stdout.field}` resolves through JSON strings without an intermediate node. * Inline object templates preserve types correctly — no more double-serialization when passing dicts. * Dicts and lists auto-coerce to JSON strings when mapped to string-typed parameters. * Optional inputs without defaults resolve correctly instead of erroring. Previously you needed an extraction step between a shell command and anything that wanted its output as structured data: ```yaml Before (v0.4.0) theme={null} ### get-data - type: shell # curl outputs JSON string ### extract-json - type: shell # jq extracts the field you need ### use-data - type: llm # finally gets the value ``` ```yaml After (v0.5.0) theme={null} ### get-data - type: shell # curl outputs JSON string ### use-data - type: llm # access nested fields directly: # ${get-data.stdout.items[0].name} ``` ## Shell node fixes Shell nodes now surface `stderr` even when the exit code is zero. Tools like `curl` and `ffmpeg` write diagnostics to stderr on success, and those warnings were getting lost. **Highlights** * `stderr` visible on successful commands, not just failures. * Trailing newlines stripped from `stdout` by default (disable with `strip_newline: false`). * Pipeline-aware error detection for `grep | sed` chains where only the last exit code was visible. * Fixed `SIGPIPE` crashes when a subprocess closed its input early. ### Explicit data wiring Nodes can no longer silently read from the shared store by key name. All data must be wired through `${variable}` templates. This prevents a class of bugs where a node ID collided with a parameter name and got the wrong value. ```yaml Before theme={null} ### transform # Could silently read "data" from shared store - type: code ``` ```yaml After theme={null} ### transform # Must declare all inputs explicitly - type: code - inputs: data: ${fetch-data.response} ``` ### Claude Code node * `task` → `prompt` * `working_directory` → `cwd` * `context` removed — include it directly in the prompt ## Validation that helps you fix things When something goes wrong, pflow now tells the agent exactly what to do instead of printing a stack trace. Wrong template path? It shows every available output with its type and suggests the correct one. ``` ✗ Node 'format-report' references unknown output 'fetch-data.email' Available outputs from 'fetch-data': ✓ ${fetch-data.response} (dict) ✓ ${fetch-data.status_code} (int) ✓ ${fetch-data.response_headers} (dict) Tip: Did you mean ${fetch-data.response}? ``` Validation runs automatically before every execution — no separate step needed. The `--validate-only` flag lets agents check a workflow without running it. **Highlights** * Template references checked against actual node outputs before execution starts. * "Did you mean?" suggestions for misspelled node names and output paths. * Type mismatch warnings when connecting incompatible outputs to inputs. * `--validate-only` flag for CI pipelines and agent pre-checks. ## Agent tooling The CLI now has discovery commands so agents can find the right building blocks without knowing what's available ahead of time. `registry discover` takes a natural language description and returns matching nodes. **Highlights** * `pflow registry discover "fetch API data and send to Slack"` returns matching nodes ranked by relevance. * `pflow registry run node-type param=value` tests individual nodes outside of a workflow — output is pre-filtered for agents, showing structure without data. * `pflow instructions usage` gives agents a complete guide to pflow's commands and patterns. * Allow/deny filtering via `pflow settings` to control which nodes are available. ```bash theme={null} # Agent gets a task from the user # Step 1: Check if a workflow already exists pflow workflow discover "analyze git commits and post to slack" # Step 2: No match — find building blocks pflow registry discover "git log, LLM classification, slack message" # Step 3: Check a node's output structure pflow registry run mcp-composio-slack-SLACK_SEND_MESSAGE \ channel="test" markdown_text="hello" # Step 4: Build the workflow using discovered nodes ``` ## MCP server improvements Connecting external tools got more reliable. Server configs now expand environment variables everywhere (URLs, headers, auth fields), and sync only runs when something actually changed. **Highlights** * Environment variables expanded in all MCP config fields, not just API keys. * Smart sync skips re-scanning when server configs haven't changed (\~500ms saved on warm starts). * HTTP transport support for remote MCP servers alongside stdio. * Better error messages when MCP servers fail to start or authenticate. ## Workflow engine Write a `.pflow.md` file, run it from the terminal. Steps execute top to bottom, data flows between them through template variables. Save it with `pflow workflow save` and it becomes a command you can run from anywhere. ````markdown theme={null} ### fetch-users Get active users from the API. - type: http - url: https://api.example.com/users?status=active ### filter-active Keep only users who logged in this month. - type: code - inputs: users: ${fetch-users.response.data} ```python code users: list result: list = [u for u in users if u['last_login_days'] < 30] ``` ### summarize Summarize the filtered users for the weekly report. - type: llm ```prompt Summarize these ${filter-active.result} active users for a weekly report. ``` ```` **Highlights** * Run from file path (`pflow workflow.pflow.md`) or by name (`pflow my-workflow`). * Templates reach into nested objects and arrays — `${node.result.data.users[0].email}`. * Execution traces saved to `~/.pflow/debug/` with per-node inputs, outputs, and timing. * Pipe workflows together: `pflow -p workflow-a | pflow -p workflow-b`. ## Built-in nodes Eight node types that cover the common building blocks. MCP bridges to anything else — GitHub, Slack, databases, whatever has an MCP server. **Highlights** * `shell` — run commands with dangerous-pattern blocking and timeouts. * `code` — inline Python with native object passing (no serialization overhead). * `llm` — any model via Simon Willison's [llm](https://llm.datasette.io/) library, with token tracking. * `http` — all methods, auth, request bodies, automatic JSON parsing. * `file` — read, write, copy, move, delete. * `mcp` — bridge to any MCP server over stdio or HTTP transport. * `agent` — delegate agentic subtasks to Claude Code or Codex. * `git` / `github` — common operations without shell scripting. ## MCP server pflow itself runs as an MCP server, so agents in Claude Desktop, Cursor, or any MCP-compatible environment can build and run workflows programmatically. **Highlights** * 11 tools covering workflow execution, node discovery, and registry inspection. * Structure-only output mode — agents see schema types without actual data, keeping context windows small. * Works alongside CLI usage. Same workflows, same registry, different interface. ```bash theme={null} uv tool install pflow-cli ``` ```bash theme={null} pflow settings set-env ANTHROPIC_API_KEY "sk-ant-..." ``` ```bash theme={null} pflow workflow.pflow.md ``` Or tell your agent to run `pflow instructions usage` — it gets everything it needs to discover, build, and run workflows. Batch processing for fan-out patterns, smarter template resolution, and shell node reliability improvements. See the [Roadmap](/roadmap). # Adding MCP servers Source: https://docs.pflow.run/guides/adding-mcp-servers Expand pflow capabilities with external tools MCP (Model Context Protocol) servers let you add external tools to pflow. Once added, your AI agent can use these tools in workflows - GitHub, Slack, databases, and more. pflow supports both **local (stdio)** and **remote (HTTP)** MCP servers. ## Adding a server ### From a config file If you have an MCP config file (JSON format): ```bash theme={null} pflow mcp add ./github.mcp.json ``` Config file format: ```json theme={null} { "github": { "command": "npx", "args": ["-y", "@github/mcp-server"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "your-token" } } } ``` ### From JSON directly For quick setup, pass JSON directly: ```bash theme={null} # Local stdio server pflow mcp add '{"github": {"command": "npx", "args": ["-y", "@github/mcp-server"]}}' # Remote HTTP server pflow mcp add '{"slack": {"type": "http", "url": "https://mcp.example.com/slack"}}' ``` ### Multiple servers at once Add multiple servers from separate files: ```bash theme={null} pflow mcp add ./github.mcp.json ./slack.mcp.json ./notion.mcp.json ``` ## Tools are auto-discovered You don't need to manually sync after adding a server. When your agent runs a workflow, pflow automatically discovers tools from any new or changed servers. If you want to test the connection immediately after adding a server, run `pflow mcp sync ` to force discovery. ## Managing servers ### List configured servers ```bash theme={null} pflow mcp list ``` ### View available tools See all tools from a specific server: ```bash theme={null} pflow mcp list github ``` Or list all tools from all servers: ```bash theme={null} pflow mcp list ``` ### Get tool details See detailed information about a specific tool: ```bash theme={null} pflow mcp describe mcp-github-create-issue ``` ### Remove a server ```bash theme={null} pflow mcp remove github ``` ## Configuration file location pflow stores MCP server configurations in: ``` ~/.pflow/mcp-servers.json ``` You can edit this file directly instead of using `pflow mcp add`. After manual edits, run `pflow mcp sync ` or `pflow mcp sync --all` to register the changes. The file uses the [standard MCP configuration format](https://modelcontextprotocol.io/docs/develop/connect-local-servers) used by Claude Desktop, VS Code, and other MCP clients. ## Server configuration format ### Local (stdio) servers Local servers run as subprocesses on your machine. The `type` field is optional and defaults to `"stdio"`: ```json theme={null} { "server-name": { "command": "npx", "args": ["-y", "@namespace/mcp-server"], "env": { "API_KEY": "your-key" } } } ``` | Field | Required | Description | | --------- | -------- | -------------------------------------------------- | | `command` | Yes | The command to run (e.g., `npx`, `python`, `node`) | | `args` | No | Command arguments | | `env` | No | Environment variables passed to the server | ### Remote (HTTP) servers Remote servers connect over HTTP/SSE. The `type` field is **required** for HTTP servers: ```json theme={null} { "server-name": { "type": "http", "url": "https://mcp.example.com/server", "headers": { "Authorization": "Bearer ${API_TOKEN}" } } } ``` | Field | Required | Description | | --------- | -------- | ----------------------------------- | | `type` | Yes | Must be `"http"` for remote servers | | `url` | Yes | The server URL (SSE endpoint) | | `headers` | No | HTTP headers for authentication | ### Environment variable expansion Use `${VAR}` syntax to reference environment variables in your config: ```json theme={null} { "github": { "command": "npx", "args": ["-y", "@github/mcp-server"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } } ``` Variables are resolved from two sources (in order of precedence): 1. **System environment** - `export GITHUB_TOKEN="your-token"` 2. **pflow settings** - `pflow settings set-env GITHUB_TOKEN "your-token"` Use `${VAR:-default}` for fallback values: ```json theme={null} { "url": "${API_URL:-https://api.example.com}" } ``` Variables are expanded at runtime when the server starts, not when the config is saved. ## Using MCP tools in workflows Once added, MCP tools become pflow nodes that your agent can use: ```bash theme={null} # Discover relevant tools pflow mcp find "create github issues" # See tool parameters pflow mcp describe mcp-github-create_issue ``` See [MCP nodes](/reference/nodes/mcp) for how these nodes work in workflows - naming convention, parameters, output format, and examples. If you're repeatedly calling the same API using the [http node](/reference/nodes/http), consider having your agent create an MCP server for it. This turns one-off HTTP requests into reusable, discoverable tools. ## Common MCP servers Here are some popular MCP servers. Save any of these as a `.json` file and add with `pflow mcp add ./filename.json`: ### GitHub ```json theme={null} { "github": { "command": "npx", "args": ["-y", "@github/mcp-server"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" } } } ``` See [github/github-mcp-server](https://github.com/github/github-mcp-server) for full documentation. ### Filesystem For basic file operations, pflow includes built-in [file nodes](/reference/nodes/file) - no MCP server needed. The filesystem MCP server is useful for advanced operations or stricter directory sandboxing. If you do use it, consider disabling the built-in file nodes to avoid confusion: `pflow settings deny "pflow.nodes.file.*"`. ```json theme={null} { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/directory"] } } ``` ### Brave Search ```json theme={null} { "brave-search": { "command": "npx", "args": ["-y", "@brave/brave-search-mcp-server"], "env": { "BRAVE_API_KEY": "${BRAVE_API_KEY}" } } } ``` See [Brave Search API](https://brave.com/search/api/) for API key setup (free tier: 2,000 queries/month). Find more MCP servers at the [Official MCP Registry](https://registry.modelcontextprotocol.io) or the community-curated [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers) list. ## Troubleshooting 1. Check your JSON syntax is valid 2. Make sure the config file path is correct 3. Run `pflow mcp servers` to see configured servers 1. Check the server is running correctly: `pflow mcp sync ` 2. Verify credentials/environment variables are set 3. Check the server logs for errors 1. Run `pflow mcp describe ` to see required parameters 2. Check that required environment variables are set in the server config 3. Verify the server has access to required resources (network, files, etc.) # How debugging works Source: https://docs.pflow.run/guides/debugging How pflow helps agents fix broken workflows When workflows fail, pflow gives your agent structured error data — available fields, types, and fix suggestions. Since agents build and fix workflows, error messages are the primary interface. They say what's wrong, what's available instead, and how to fix it. Your role in debugging is minimal. Most of the time, you don't need to do anything. ## Your agent handles most debugging When a workflow fails, your agent receives detailed information about what went wrong: * **What failed** - Which node, what error category * **What's available** - The fields that DO exist (not just what's missing) * **Suggestions** - "Did you mean?" recommendations * **Execution state** - Which nodes succeeded before the failure Your agent uses this to self-correct. When building or fixing workflows, it reads pflow's built-in instructions — how to interpret errors, inspect trace files, and resolve common issues. You don't need to teach your agent how to debug. The guidance is built in. ## What your agent sees When a workflow fails, errors include the data needed for self-correction: ```json theme={null} { "errors": [{ "message": "Node 'fetch' does not output 'msg'", "node_id": "process", "available_fields": ["result", "result.messages", "result.messages[0].text"], "fixable": true }] } ``` Your agent sees that `msg` doesn't exist, but `result.messages` does. It fixes the template and retries. No human intervention needed. These errors are specific because the error space is finite — pflow knows every node type and every declared output. When agents compose known building blocks instead of writing arbitrary code, errors say "node X doesn't output Y, did you mean Z?" instead of handing you a stack trace. ## Execution reports The `--report` flag generates a structured execution report — a directory of readable markdown files, one per node: ```bash theme={null} # Report to default location (~/.pflow/reports/{name}/) pflow my-workflow --report # Report to a specific directory pflow my-workflow --report-dir ./report/ ``` The report includes: * **summary.md** — pipeline table with per-node cost, errors with fix suggestions, anomaly warnings for suspicious empty outputs * **Per-node files** — rendered prompts (what the LLM actually received), full responses, model/token/cost metadata * **Batch items** — per-item files showing individual prompts and responses * **Sub-workflows** — nested directories mirroring the workflow structure Reports are ephemeral — the default location is overwritten each run. The trace files are the durable history. You can regenerate a report from any previous run: ```bash theme={null} # Most recent trace pflow report # A specific past run pflow report ~/.pflow/debug/workflow-trace-my-workflow-20260323-150000.json -o /tmp/old ``` Report directories are generated snapshots. pflow replaces the whole directory when writing a report so old node pages cannot be mistaken for current-run evidence. Custom report directories must be empty or already contain `.pflow-report.json`, the marker pflow writes into report output. ### Comparing runs with git diff Write reports to a project-local folder. Stage the report, edit a prompt, re-run — `git diff` shows exactly what changed: ```bash theme={null} pflow my-workflow --report-dir ./report/ git add report/ # Edit a prompt, re-run pflow my-workflow --report-dir ./report/ git diff report/ ``` Each node gets its own file, so you can diff just the node you changed. With stochastic LLM outputs, a full-report diff is noisy — targeted per-node diffs show whether your prompt change had the intended effect. ## Trace files pflow automatically saves detailed execution traces: * **Location**: `~/.pflow/debug/workflow-trace-*.json` * **When**: Every workflow run (success or failure) * **Content**: Per-node timing, inputs, outputs, template resolutions, errors Traces are the raw data behind execution reports. Your agent can read these directly when it needs programmatic access, but the `--report` output is usually more useful for debugging. Traces are saved automatically. Use `--no-trace` if you want to disable this (the `--report` flag overrides `--no-trace`). ## What only you can fix Some things require human action. Your agent will tell you when these come up: ### API key not configured If pflow's discovery features aren't working: ```bash theme={null} pflow settings set-env OPENAI_API_KEY "sk-..." ``` pflow auto-detects available providers. You can optionally override the model: ```bash theme={null} pflow settings llm set-default openai/gpt-5.2 ``` Your agent can't configure API keys for security reasons, but it will tell you exactly what command to run. ### MCP server issues If your agent reports MCP tools aren't available: ```bash theme={null} # Check what servers are configured pflow mcp list # Force re-sync if needed pflow mcp sync --all ``` See [adding MCP servers](/guides/adding-mcp-servers) for setup details. ### Disk cleanup Trace files accumulate over time. pflow doesn't auto-delete them. If disk space becomes an issue: ```bash theme={null} # Remove old traces (check contents first if needed) rm ~/.pflow/debug/workflow-trace-*.json ``` ## Summary | Situation | Who handles it | | --------------------------------- | --------------------------------- | | Workflow fails with fixable error | Your agent (self-corrects) | | Agent needs more context | Your agent (reads trace files) | | API key not configured | You (agent tells you the command) | | MCP server not connected | You (agent guides you) | | Disk space from traces | You (manual cleanup) | pflow is built for self-correction. Your agent has the tools and knowledge to debug most issues — you only step in for setup tasks that require human access. # Publishing skills Source: https://docs.pflow.run/guides/publishing-skills Make workflows automatically available to your AI agent pflow workflows can be published as [Agent Skills](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills). Skills are loaded automatically when your AI agent starts — no need to run `pflow list` or `pflow find`. Your agent just knows the skill exists and can use it. When you publish a workflow as a skill, pflow creates a symlink from the tool's skill directory (e.g., `.claude/skills/`) to your saved workflow. Your workflow stays in `~/.pflow/workflows/` as the single source of truth, and updates automatically appear in all linked skills. ## Publishing a workflow Make sure your workflow is saved to the library: ```bash theme={null} pflow save ./my-workflow.pflow.md --name my-workflow ``` ```bash theme={null} pflow skill save my-workflow ``` This creates a symlink at `.claude/skills/my-workflow/SKILL.md` and enriches your workflow with usage instructions for your agent. By default, skills are saved to the **project directory** (`.claude/skills/`). Use `--personal` for skills you want available across all projects. ## Publishing to multiple tools Publish to different AI tools using flags: ```bash theme={null} # Publish to Cursor pflow skill save my-workflow --cursor # Publish to multiple tools at once pflow skill save my-workflow --cursor --copilot ``` See [supported tools](/reference/cli/skill#supported-tools) for all available targets. ## Project vs personal skills **Project skills** (default) live in your project directory and are typically committed to version control. Team members who clone the repo get the same skills. **Personal skills** live in your home directory and are available across all projects: ```bash theme={null} pflow skill save my-workflow --personal ``` ## Managing skills List all skills: ```bash theme={null} pflow skill list ``` Remove a skill: ```bash theme={null} pflow skill remove my-workflow ``` See [pflow skill reference](/reference/cli/skill) for all options including `--cursor`, `--copilot`, and `--personal` flags. ## How enrichment works When you publish a skill, pflow adds a `## Usage` section to your workflow with instructions for your agent, including how to run it and how to check execution history: ``` pflow my-workflow param1= pflow history my-workflow ``` This enrichment is **idempotent** — running `skill save` again just updates the usage section. When you update a workflow with `pflow save --force`, pflow automatically re-enriches it. Skills shouldn't be static installs. When your needs change, your agent modifies the workflow and re-publishes — the skill updates automatically because it's a symlink to the source. Over time, your skills evolve with your usage rather than going stale. ## Best practices **Publish stable workflows.** Skills are meant for workflows you want to reuse. Publish once a workflow is working reliably. **Use project skills for team workflows.** Commit `.claude/skills/` to version control so your team shares the same automation. **Use personal skills sparingly.** Reserve these for truly universal workflows you want everywhere. ## Related * [pflow skill reference](/reference/cli/skill) - Full command documentation * [Using pflow](/guides/using-pflow) - How agents use pflow * [Claude Code integration](/integrations/claude-code) - Tool-specific setup * [Cursor integration](/integrations/cursor) - Tool-specific setup # Using pflow Source: https://docs.pflow.run/guides/using-pflow What to expect after setup Once pflow is installed and connected to your AI tool, you don't need to learn anything else. Your agent has everything it needs to build and run workflows. ## You don't need to learn the schema pflow workflows are markdown files (`.pflow.md`) — readable documents that double as executable workflows. Your agent writes them, but you can open any workflow to see exactly what it does and why. Just like any `.md` file, it renders everywhere — GitHub, your editor, any markdown viewer. Your agent: * Reads pflow's instructions automatically * Knows which nodes are available * Understands how to connect them * Handles all the technical details * Creates, updates and reuses workflows for you You just describe what you want in natural language. Your agent does the rest. ## Your agent guides you when needed When pflow needs something from you, your agent will tell you exactly what to do. **Need an API key?** > Your agent: "pflow needs an API key for discovery. Run this command: `pflow settings set-env OPENAI_API_KEY 'your-key'`" **Need to connect to an API?** > Your agent reads the documentation and builds the integration for you using the [http node](/reference/nodes/http). **Need an MCP server?** > Your agent helps you find and install it with your permission. If you're calling the same API repeatedly, consider having your agent create an MCP server for it - turning one-off requests into reusable tools. **Something unexpected happen?** > Your agent diagnoses the issue using pflow's structured errors and traces. See [How debugging works](/guides/debugging) for details. You don't need to memorize *any* commands. Your agent knows them and will prompt you when necessary. ## What happens behind the scenes When you ask your agent to do something: 1. **Agent checks for existing workflows** - If you've done this task before, pflow finds the saved workflow 2. **Runs instantly if found** - No repeated workflow generation, no LLM costs, same reliable result 3. **Updates existing workflow if it needs to be modified** - Agent updates it with new functionality or parameters 4. **Builds new workflow if needed** - Agent creates it once, pflow saves it for next time Over time, your workflow library grows. Tasks that used to require agent reasoning become instant commands. ## Not everything needs a workflow Sometimes you just need to run a single tool - send a Slack message, fetch a file, query an API. Your agent can run individual nodes directly without building a workflow: ```bash theme={null} pflow probe mcp-slack-SEND_MESSAGE channel="general" text="Done!" ``` This is useful for: * **One-off tasks** - No workflow needed, just run the tool * **Testing** - Your agent tests nodes to understand their output before building workflows Think of nodes as individual building blocks. Your agent can use them standalone or compose them into workflows - whatever fits the task. ## Checking your workflow library To see what workflows have been saved: ```bash theme={null} pflow list ``` To see details about a specific workflow: ```bash theme={null} pflow describe my-workflow ``` See [workflow commands](/reference/cli/list) for more options. ## Optional: Running workflows directly You don't *have to* go through your agent. Saved workflows are CLI commands you can run yourself: ```bash theme={null} # Run a saved workflow pflow analyze-logs input=./logs/api.log # Pipe data through a workflow cat data.csv | pflow -p --output-format json process-csv > output.json # Schedule with cron 0 9 * * * pflow daily-report >> ~/reports/daily.md ``` This is useful for: * **CI/CD pipelines** - Run workflows as build steps * **Cron jobs** - Schedule recurring tasks * **Scripts** - Chain workflows with other tools See [CLI overview](/reference/cli/index) for all options. ### Unix piping Chain workflows together using pipes: ```bash theme={null} pflow -p step1 | pflow -p step2 | pflow step3 ``` See [Stdin input](/reference/cli/index#stdin-input) for how workflows receive piped data. ## Summary | Task | Who handles it | | ---------------------------- | --------------------------------- | | Learning pflow commands | Your agent | | Writing workflows | Your agent | | Knowing which nodes exist | Your agent | | Configuring API keys | You (agent tells you the command) | | Discovering new capabilities | Your agent or you | | Installing new MCP servers | Your agent or you | | Running saved workflows | Your agent or you directly | You describe what you want done. Your agent does the thinking. pflow handles repeated execution — same steps, same order, same data flow, every time. The workflows your agent builds become your personal library, ready to reuse or chain into larger workflows. # Approval Gates Source: https://docs.pflow.run/how-it-works/approval-gates Pause a workflow for a human decision with approval: required Add `approval: required` to any step and the run pauses before that step, shows what is about to happen, and waits for a yes/no. This is how you trust a workflow with real-world actions — sending messages, creating issues, deploying — without giving up the final say. ```md theme={null} ### notify-slack Post the release summary to Slack. - type: mcp-composio-slack-SLACK_SEND_MESSAGE - channel: ${slack_channel} - markdown_text: ${create-summary.result} - approval: required ``` At a terminal: ```text theme={null} ⏸ Approval required: notify-slack (mcp-composio-slack-SLACK_SEND_MESSAGE) channel: #releases markdown_text: v0.9.0 released with 3 features: … Run this step? [y/N]: ``` The preview shows **resolved values** — the actual message text, not `${...}` templates. You approve what will happen, not the abstract definition. ## Denying Answering no stops the run cleanly **before the step runs**: nothing fired, nothing failed. The exit code is `3` (distinct from `1` for failures — a human verdict is not an error), and the trace records `final_status: "denied"`. ## Pre-approving gates For CI or agent-operated runs, pre-approve specific gates by step name: ```bash theme={null} pflow my-workflow --auto-approve=notify-slack ``` The flag is per-step and repeatable — there is deliberately no approve-everything form. Each pre-approval is recorded in the trace (`resolved_via: flag`), so you can always audit which gates a human answered live versus approved in advance. ## Non-interactive runs pause — and you answer later A run with no terminal (piped, launched from the web UI, or via the MCP server) cannot prompt. A gate reached without pre-approval **pauses the run durably** instead of failing — never hangs — and prints a resume token: ```text theme={null} Paused at 'notify-slack'. Resume token: 7a9e4afb-2095-447e-8043-0660438104f9 (exit 4) ``` Nothing re-runs when you answer: the completed steps are kept, and the decision can come hours or days later, through either surface: * **In the browser**: `pflow ui --run ` opens the paused run on the visual canvas. The paused step carries a ⏸ badge with an answer panel showing the step's resolved values — click **Approve** or **Deny** (for an escalation: pick an option or type an answer) and watch the run continue in place. * **From the terminal**: `pflow resume --approve yes|no` (escalations: `--choose ""`). `pflow resume list` shows every pending pause. A deny still exits `3` with `final_status: "denied"` — a human verdict, not an error. Each answered pause produces a new attempt linked to the original run, so the decision trail stays auditable. A warning also fires at run start when the workflow has unapproved top-level gates. Use `--dry-run` to discover gates before running: gated steps are tagged `[…, approval]` and summarized in a `⏸` footer. ## Escalations — the agent asks you The second gate kind is raised by an agent step at runtime instead of declared by you. When an `agent` step discovers a decision it should not make alone, it returns an `escalation` object (question, options, recommendation) in its structured output — the run pauses, you choose (pick an option or type an answer), and your decision is written back into the step's result for the workflow to act on, typically by re-running the agent with the decision via `loop:`. See `pflow guide approval` for the escalation contract, the re-fork recipe, and the full rules (batch steps can't be gated — gate around them; loop steps prompt every iteration; cached steps never prompt). # Batch processing Source: https://docs.pflow.run/how-it-works/batch-processing Process arrays of items through workflows **For the curious.** Your AI agent configures batch processing when needed. This explains what happens when you ask to process many items (files, API results, etc.) and what to expect during execution. Batch processing runs a single node multiple times — once for each item in an array. Think for-loop, but declarative: your agent adds a batch config to any node, and pflow handles the looping, concurrency, and error collection. ## When batch processing happens Your agent uses batch processing when tasks involve: * Processing each file in a directory listing * Analyzing each item from an API response * Running the same LLM prompt on multiple inputs * Transforming each element in an array **Example scenario:** When you ask to classify 100 GitHub issues, your agent configures a batch node to process each issue. ## How it works A `batch` configuration is added to a node: ```markdown theme={null} ## Steps ### list_issues Fetch issues from the GitHub API. - type: http - url: https://api.github.com/repos/owner/repo/issues ### classify Classify each issue by type. - type: llm - prompt: Classify this issue: ${issue.title} - batch: items: ${list_issues.response} as: issue ``` This runs the `classify` node once for each issue. The `as: "issue"` creates a template variable `${issue}` that changes with each iteration. ## Configuration options | Field | Type | Required | Default | Description | | ---------------- | -------- | -------- | ------------- | ------------------------------------------------------------------------------------ | | `items` | template | Yes | - | Array to iterate over (usually `${previous_node.key}`) | | `as` | string | Yes | - | Name for the item variable (e.g., `"item"`, `"file"`, `"issue"`) | | `parallel` | bool | No | `false` | Run items concurrently instead of sequentially | | `max_concurrent` | int | No | `10` | Maximum parallel items (1-100) | | `error_handling` | string | No | `"fail_fast"` | `"fail_fast"` or `"continue"` | | `max_retries` | int | No | `1` | Batch item total attempts after an exception escapes the node (`1` = no batch retry) | | `retry_wait` | number | No | `0` | Seconds to wait between batch item attempts | ## Sequential vs parallel ### Sequential (default) Items are processed one at a time, in order: ```markdown theme={null} ### process Read each file sequentially. - type: read-file - file_path: ${file} - batch: items: ${files} as: file ``` This mode is chosen when: * Order matters * Rate limits are strict * Resources are limited ### Parallel Multiple items are processed concurrently: ```markdown theme={null} ### process Read each file in parallel. - type: read-file - file_path: ${file} - batch: items: ${files} as: file parallel: true max_concurrent: 5 ``` This mode is chosen when: * Items are independent * Speed is important * API/LLM can handle concurrent requests Your agent typically starts with `max_concurrent: 5` for LLM calls to avoid rate limits, increasing gradually based on API tier. ## Error handling ### Fail fast (default) Execution stops immediately on first error: ```markdown theme={null} ### process Process each file, stopping on first error. - type: read-file - file_path: ${file} - batch: items: ${files} as: file error_handling: fail_fast ``` This mode is chosen when: * Any failure means the whole task is invalid * Errors should be fixed and re-run from scratch ### Continue on errors All items are processed, with errors collected: ```markdown theme={null} ### process Process each file, continuing on errors. - type: read-file - file_path: ${file} - batch: items: ${files} as: file error_handling: continue ``` This mode is chosen when: * Partial results are useful * Some failures are expected * All errors should be seen before fixing The node output includes error details in this mode: ```json theme={null} { "results": [...], "errors": [ { "index": 3, "item": "file3.txt", "error": "File not found" } ] } ``` ## Retries Failed items can be automatically retried: ```markdown theme={null} ### process Process each API call with retries and error tolerance. - type: http - url: ${call.url} - batch: items: ${api_calls} as: call parallel: true max_retries: 3 retry_wait: 2 error_handling: continue ``` This configuration gives each failed item up to 3 total batch attempts, waiting 2 seconds between attempts. Common in scenarios involving: * Transient API errors * Rate limit recovery * Network timeouts Node-level `retry:` is separate from batch retry and applies inside each node attempt: ```markdown theme={null} ### fetch Fetch each API call with exponential node backoff. - type: http - url: ${call.url} - retry: max: 3 wait: 0.5 backoff: exponential - batch: items: ${api_calls} as: call parallel: true ``` For nodes that return an `"error"` action when exhausted (`llm`, `shell`, `mcp`, `code`, file nodes), batch does not run another item attempt. Attempts multiply only for nodes that re-raise after node retries are exhausted, such as `http`, `agent`, or custom nodes using the default fallback. ## What you'll see During batch execution, pflow shows real-time progress: ``` fetch-issues... ✓ 2.1s classify... 1/8 ✓ classify... 2/8 ✓ classify... 3/8 ✗ ... classify... 8/8 ✓ 24.9s ``` Failed items are marked with `✗` and summarized at the end. When a failed item is large, pflow shows a compact description instead of printing the full item. For example, a record with a label and a long payload is shown with the label, payload size, and a stable reference: ```text theme={null} Batch 'classify' errors: [3] File not found item: label='issue-42'; payload= ``` The original failed input remains available in runtime data and trace files for debugging. Terminal output, MCP output, JSON error responses, and generated reports use the compact form so the actionable error stays visible. ## Output structure Batch nodes write a special output structure to the shared store: ```json theme={null} { "node_id": { "results": [ {"item": "input1", "response": "..."}, {"item": "input2", "response": "..."} ], "count": 3, "success_count": 2, "error_count": 1, "batch_metadata": { "parallel": true, "timing": { "total_items_ms": 24900, "avg_item_ms": 3112 } }, "errors": [ {"index": 2, "item": {}, "error": "..."} ] } } ``` `results` contains only **successful** items — each pairs `item` (the original input) with the inner node's outputs. With `error_handling: continue`, failed items are excluded from `results` and appear only in `errors`. `count` is the total items attempted, `success_count` equals `len(results)`, and `error_count` equals `len(errors)`. Inside a batch node, `${__index__}` gives the 0-based position of the current item. Index-based access to results (like `${node.results[0].field}`) requires `fail_fast` mode (the default). With `error_handling: continue`, use iteration (`items: ${node.results}`) instead — the validator blocks index access because filtered results don't preserve original positions. Subsequent nodes can access results: ```markdown theme={null} ### summarize Summarize all the classifications. - type: llm - prompt: Summarize these classifications: ${classify.results} ``` ## Examples ### Process files from directory listing ````markdown theme={null} ## Steps ### list List all markdown files. - type: shell ```shell command ls -1 *.md ``` ### split Convert the file listing into a JSON array. - type: shell - stdin: ${list.stdout} ```shell command tr '\n' ',' | jq -Rc 'split(",") | map(select(length > 0))' ``` ### read_all Read each file in parallel. - type: read-file - file_path: ${filename} - batch: items: ${split.stdout} as: filename parallel: true max_concurrent: 10 ```` ### API pagination pattern ````markdown theme={null} ## Steps ### get_pages Generate a list of page numbers. - type: shell ```shell command echo '[1,2,3,4,5]' ``` ### fetch_all Fetch each page of results in parallel. - type: http - url: https://api.example.com/items?page=${page} - batch: items: ${get_pages.stdout} as: page parallel: true max_concurrent: 3 ```` ### Fault-tolerant LLM processing ```markdown theme={null} ### process Summarize each document with retries and error tolerance. - type: llm - prompt: Summarize: ${doc.content} - model: openai/gpt-4 - batch: items: ${documents} as: doc parallel: true max_concurrent: 5 max_retries: 3 retry_wait: 2 error_handling: continue ``` ### Per-item configuration ````markdown theme={null} ### compare Compare quality across model configurations. - type: llm - model: ${config.model} - reasoning_effort: ${config.effort} - prompt: "Analyze this data: ${config.data}" ```yaml batch items: - data: ${report} model: anthropic/claude-opus-4-5 effort: high - data: ${report} model: openai/gpt-5.2 effort: medium as: config parallel: true ``` ```` Each item can override any node parameter through template variables — not just the prompt. Here `model` and `reasoning_effort` change per item while the prompt template stays the same. ## How your agent chooses settings **For LLM calls**, your agent typically: * Starts with `max_concurrent: 5` * Monitors rate limits and costs * Uses `retry_wait` for rate limit recovery **For HTTP requests**, your agent typically: * Checks API rate limits in documentation * Uses `max_concurrent` to respect limits * Adds retries for transient errors **For file operations**, your agent typically: * Uses parallel processing for reads (safe) * Uses sequential mode for writes (avoids race conditions) * Uses sequential mode when files depend on each other ## Limitations * **No nested batch** - You can't batch a node that's already in a batch * **No branching within batch** - Each item follows the same code path * **Memory usage** - All results are held in memory until batch completes ## Related * [Template variables](/how-it-works/template-variables) - Understanding `${item}` variables * [Shell node](/reference/nodes/shell) - Often used to prepare arrays * [HTTP node](/reference/nodes/http) - API pagination patterns * [LLM node](/reference/nodes/llm) - Batch prompt processing # Loops Source: https://docs.pflow.run/how-it-works/loops Condition-terminated and stateful iteration with loop: `loop:` repeats a single node until its own typed output says to stop. It is a do-while loop: the node runs once, then pflow evaluates the condition. ```md theme={null} ### wait - type: workflow - workflow: ./check-status.pflow.md - inputs: job_id: ${job_id} - loop: until: ${wait.done} max_iterations: 60 ``` Use exactly one condition: * `while: ${node.more}` continues while the value is truthy. * `until: ${node.done}` continues while the value is falsy. Conditions must be single template references to typed outputs. Put comparisons inside the loop body and output a boolean. ## Carried State Use `carry:` when the next iteration needs state produced by the previous one. The node's `inputs:` are the round-1 seed. On round 2 and later, `carry:` overrides only the carried keys. ```md theme={null} ### run-rounds - type: workflow - workflow: ./judge-round.pflow.md - inputs: contenders: ${initial_lineup} - loop: carry: contenders: ${run-rounds.survivors} while: ${run-rounds.more} max_iterations: 100 ``` Each `carry:` value must reference this loop node's latest output. A missing or typoed carried output is a runtime error even when template resolution is permissive — the error names the loop node and its available outputs. A literal coalesce fallback breaks that guarantee, so avoid it in `carry:`. With `carry: { contenders: ${run-rounds.survivors ?? []} }`, the `[]` always resolves, so a round where the body omits `survivors` silently re-seeds `contenders` to `[]` instead of erroring. Validation warns when a carry value uses a literal fallback. A round-1 default belongs in `inputs:`, not in a carry fallback. For `shell` and `llm` nodes, carried keys are effective only when referenced in `command`, `prompt`, or `system` text. Code and workflow nodes consume `inputs:` directly. ## Caps And Inspection `max_iterations` is optional and defaults to the visit guard. Hitting the cap is not a failure; pflow stamps `loop_stopped: "max_iterations"` on the loop output. A condition stop stamps `loop_stopped: "condition"`. `--only ` runs exactly one iteration with round-1 seed inputs. It does not reproduce a mid-loop carried state. # Prompt caching Source: https://docs.pflow.run/how-it-works/prompt-caching Cut input cost on workflows that share context across LLM calls **For the curious.** Your AI agent declares cache blocks when a workflow has multiple LLM calls sharing context. This explains what those declarations do and why they reduce cost. When a workflow chains many LLM calls that all reference the same context (a project brief, a long document, a fixed persona), the LLM provider re-tokenizes that context on every call. With provider-side prompt caching, that context gets tokenized once on the first call and read from a cached prefix on every subsequent call within the TTL window. Cached reads cost \~10% of normal input tokens on Anthropic and Gemini; OpenAI auto-caches at no extra rate. On a workflow with 15 sequential LLM calls sharing a 10k-token context, this typically cuts input cost 50-70% on the first run and 80%+ on reruns within the TTL. ## Two cache layers pflow has two independent cache layers. Don't confuse them — they solve different problems. | Layer | What it is | How to opt out | | ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | **Memo cache** | pflow's local re-execution cache. Skips a node entirely if its inputs match a prior run. | `cache: false` per node, or `--no-cache` on `pflow run` | | **Provider prompt cache** | Anthropic / OpenAI / Gemini server-side caching of the system prompt prefix. Reduces input cost; the LLM still runs. | Don't declare `## Cache` / `prompt_cache:` | `--no-cache` only disables the memo layer. Provider prompt caching still fires when declared. ## Declaring shared context The `## Cache` block sits alongside `## Inputs` and `## Steps` in a workflow file. It lists stable values that flow into multiple LLM calls — workflow inputs and upstream node outputs: ````markdown theme={null} ## Cache - ttl: 5m ```cache The project brief we are working from: ${brief} The technical constraints: ${constraints.response} ``` ```` Each chunk is one prose label followed by exactly one `${var}` reference. The prose travels into the cached system prefix verbatim — what you write is what the LLM sees as the cache label. LLM nodes opt in via `prompt_cache:`, listing chunks **in the same order** they appear in the `## Cache` block: ```markdown theme={null} ### review Review the proposed solution. - type: llm - prompt_cache: [brief, constraints.response] - prompt: Review this approach: ${approach} ``` Out-of-order subsets are a hard error at validation time — providers cache by prefix, so order has to match. The error message shows the expected order so the fix is mechanical. ## TTL | Value | Meaning | When to use | | -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------ | | `5m` (default) | pflow's default cache duration | Most workflows; matches typical run time | | `1m`-`60m` | Gemini minute-level cache duration, capped at 1 hour | Gemini workflows that need a specific retention window | | `1h` | Alias for `60m`; Anthropic 1-hour cache, Gemini 3600s, OpenAI 24h retention | Long-running workflows or reruns within an hour | Anthropic and OpenAI only support pflow's discrete `5m` and `1h` behaviors today. pflow errors instead of silently rounding unsupported minute-level TTLs. `1h` writes cost roughly 2× the standard write rate on Anthropic, so it pays off when the cached prefix gets read at least 3 times within the hour. ## Minimum tokens Provider caches only fire above a minimum token threshold: | Provider | Threshold | | ------------------------------------------ | --------- | | Anthropic Sonnet 4.5, Opus 4.1, Sonnet 3.7 | 1024 | | Anthropic Sonnet 4.6, Haiku 3.5 | 2048 | | Anthropic Opus 4.5+, Haiku 4.5 | 4096 | | Gemini explicit cached content | 4096 | | OpenAI auto-cache | 1024 | Below the threshold, pflow strips or treats the provider cache marker as ineffective before cost projection — no savings should be assumed. `pflow analyze-cache` shows below-min `ready`/`upside` rows with a blocker note explaining the threshold; only the active (provider-effective) portion feeds cost estimates. ## Batch prefix caching When a batch node fans out N parallel LLM calls that share a stable prefix (e.g., the same prompt template with `${item.X}` substituted), pflow can cache that prefix automatically. This requires `prewarm: true` on the batch node: ```markdown theme={null} ### score-each Score each candidate against the rubric. - type: llm - prompt: ./score.prompt.md - batch: items: ${candidates} as: candidate parallel: true max_concurrent: 5 - prewarm: true ``` With `prewarm: true`, pflow makes one short LLM call before the batch dispatches to warm up the provider's cache. All N batch items then run in parallel against the warm cache, paying cache-read prices instead of each writing the cache themselves. Without `prewarm:`, all N calls write the cache simultaneously — paying the write cost N times with no read benefit. The warmup covers two kinds of cached content: * Values declared in `## Cache` (when the batch node lists them in `prompt_cache:`). * The fixed portion of the prompt template that's the same for every item — the text before the first `${item.X}` reference. The warmup is a real LLM call with a real cost. That cost shows up in the `Cost:` line, in trace cost totals, and in `--report`. It's not counted in the "N calls" total because it's setup work pflow does for you, not one of your workflow's calls. For a 4,000-token cached prefix on Anthropic Sonnet 4.5, the warmup costs roughly $0.015 and each batch item then costs about $0.0015 (10% of an uncached call). Savings grow with prefix size and batch count. `pflow run --dry-run` and `pflow analyze-cache` recommend `prewarm: true` when the savings clear 5%. The trade-off: the warmup adds 5-10 seconds of wall time to the batch, in exchange for every real item running at cache-read prices. ## Sub-workflows Each `.pflow.md` file declares its own `## Cache` block scoped to its own inputs and step outputs. Sub-workflows do **not** inherit the parent's cache block — they declare independently so they can run standalone with caching. When a parent passes a value into a child workflow, both files can cache it independently. If the rendered prose labels are byte-identical across the boundary, the provider's cache fires across files (incidental, not orchestrated). `pflow analyze-cache` warns when prose labels diverge for the same logical value. ## Discovering opportunities `pflow analyze-cache` is the entry point for finding savings: ```bash theme={null} pflow analyze-cache workflow.pflow.md ``` It identifies LLM calls that share context, separates active cache from ready/upside opportunities, projects savings only from active cache, and emits a paste-ready `## Cache` block for greenfield workflows. See the [analyze-cache reference](/reference/cli/analyze-cache). `pflow run --dry-run` emits a one-line nudge when actionable opportunities exist — silent on optimal plans. ## What changes when caching is declared * The system message your LLM call receives starts with the rendered cache content (prose + values), followed by your `prompt:`. * pflow's memo cache key includes the rendered cache content, so changing a cached chunk's value invalidates memo entries correctly. * Trace files record cache token counts (`cache_creation_input_tokens`, `cache_read_input_tokens`) so you can see actual vs predicted ratios via `pflow analyze-cache --from-trace`. The bytes sent to the LLM match what you wrote in the workflow file — pflow doesn't rewrite or restructure prompts. Caching is a metadata layer; the prompt content itself is unchanged. # Template variables Source: https://docs.pflow.run/how-it-works/template-variables Dynamic data flow in workflows **For the curious.** Your AI agent handles template variables automatically. This explains how data flows between nodes and what's happening when you see `${variable}` syntax in workflows or traces. Template variables let nodes pass data to each other without writing glue code. When you see `${variable}` syntax in a workflow, it's pulling data from previous nodes, workflow inputs, or nested structures. ## Basic syntax The `${variable}` syntax accesses values from the shared store: ```markdown theme={null} ### summarize Summarize the content from the read node. - type: llm - prompt: ${read.content} ``` Here, `${read.content}` pulls the `content` output from the `read` node. ## Nested access Template variables can traverse deeply nested structures: ```markdown theme={null} ### extract Extract the name of the first item from the API response. - type: llm - prompt: ${api.response.items[0].name} ``` This traverses: 1. `api` node's output 2. `response` key 3. `items` array 4. First element (`[0]`) 5. `name` field ## Type preservation Template variables preserve the original data type when used alone. When combined with text, they become strings. | Template | Original value | Result | Type | | -------------------- | ---------------- | ----------------------------- | ------ | | `"${count}"` | `42` (int) | `42` | int | | `"Count: ${count}"` | `42` (int) | `"Count: 42"` | string | | `"${config}"` | `{"key": "val"}` | `{"key": "val"}` | dict | | `"Prefix ${config}"` | `{"key": "val"}` | `"Prefix {\"key\": \"val\"}"` | string | **Simple templates** (just `${var}`) preserve type. **Complex templates** (any surrounding text) become strings. ### Inline objects This type preservation makes inline object construction intuitive: ````markdown theme={null} ### process Process settings and results together. - type: shell ```yaml stdin config: ${settings} data: ${results} ``` ```` If `settings` is `{"timeout": 30}` and `results` is `{"status": "ok"}`, the resolved `stdin` is: ```json theme={null} { "config": {"timeout": 30}, "data": {"status": "ok"} } ``` Without type preservation, both would be stringified JSON requiring manual parsing. ## JSON auto-parsing When a template accesses nested fields on a JSON string, pflow automatically parses it: ````markdown theme={null} ## Steps ### fetch Fetch data from the API. - type: shell ```shell command curl https://api.example.com/data ``` ### extract Analyze the first result name from the fetched data. - type: llm - prompt: Analyze: ${fetch.stdout.results[0].name} ```` Even though `fetch.stdout` is a string containing JSON, the nested access `${fetch.stdout.results[0].name}` works because pflow: 1. Sees you're trying to access `.results` 2. Attempts to parse `stdout` as JSON 3. Traverses the parsed structure 4. Returns the value at `results[0].name` This means shell commands that output JSON work directly with template variables — no manual `json.loads()` needed. ## Workflow inputs Template variables also reference workflow inputs declared in the workflow definition: ````markdown theme={null} ## Inputs ### api_key API key for authenticating with the service. - type: string ### endpoint URL of the API endpoint to call. - type: string ## Steps ### call_api Call the API endpoint with authentication. - type: http - url: ${endpoint} ```yaml headers Authorization: Bearer ${api_key} ``` ```` When running this workflow, inputs are provided via CLI arguments: ```bash theme={null} pflow my-workflow api_key="sk-..." endpoint="https://api.example.com" ``` ### Stdin input Inputs can receive piped data by adding `stdin: true`. See [Stdin input](/reference/cli/index#stdin-input) for details. ## Array notation Array elements are accessed using bracket notation: ```markdown theme={null} ### extract Extract specific items from the results. - type: llm - prompt: First: ${results[0]}, Tag: ${data.items[2].tags[1]} ``` ## Batch processing In batch nodes, a special template variable (`${item}` by default) represents the current item: ```markdown theme={null} ### process Summarize each file. - type: llm - prompt: Summarize: ${file} - batch: items: ${files} as: file ``` The `as: "file"` creates `${file}` as the item variable. See [Batch processing](/how-it-works/batch-processing) for details. ## Coalesce operator The `??` operator returns the first operand that resolves, falling through whenever the left side **isn't there** — whether the node didn't run (a branch that wasn't taken) or the referenced field is absent on a node that did run: ```markdown theme={null} ### report Report the result from whichever branch ran. - type: llm - prompt: Result was: ${success_branch.stdout ?? fallback_branch.stdout} ``` This is particularly useful with [conditional branching](/how-it-works/conditional-branching) where only one path executes. Without coalesce, referencing a node that didn't run would be an unresolved variable error. Operands can also be JSON literals, used as a final default when nothing else resolves: ```markdown theme={null} - prompt: Retries so far: ${state.count ?? 0} - prompt: Label: ${item.label ?? "untitled"} ``` Literal operands accept numbers, double-quoted strings, `true`, `false`, `null`, and empty `[]`/`{}` (a literal string cannot contain the `??` sequence itself). A **bare** reference with no `??` fallback — `${node.field}` — still raises an unresolved-variable error when the field is missing, so genuine typos are caught; add a fallback only where absence is expected. ## Node metadata Beyond their primary outputs, some nodes expose metadata that downstream nodes can access through templates. This is useful for cost-aware workflows, debugging, or building on execution details. ### LLM token usage Both `llm` and `agent` nodes write `llm_usage` with token counts: ````markdown theme={null} ### analyze Analyze the document. - type: llm - model: openai/gpt-4o - prompt: Analyze this document: ${doc.content} ### log_cost Log the token usage from the analysis. - type: shell ```shell command echo "Model: ${analyze.llm_usage.model}, Tokens: ${analyze.llm_usage.total_tokens}" ```` ```` Available fields on `${node_id.llm_usage}`: | Field | Type | Description | |-------|------|-------------| | `model` | str | Model that was used | | `input_tokens` | int | Total input tokens, including cached prefix tokens | | `uncached_input_tokens` | int | Input tokens not covered by provider cache | | `output_tokens` | int | Output tokens generated | | `total_tokens` | int | Input + output | | `cache_creation_input_tokens` | int | Tokens used for cache creation | | `cache_read_input_tokens` | int | Tokens read from cache | | `input_token_accounting` | str | How `input_tokens` was derived. `llm` varies by provider; the Claude agent backend reports `split_cache_fields`, while Codex reports `total_includes_cache`. | ### Agent metadata The `agent` node includes additional execution metadata in `llm_usage` beyond the standard token fields: | Field | Type | Description | |-------|------|-------------| | `cost_usd` | null | Paid provider cost is unavailable for agent backends | | `api_equivalent_cost_usd` | float/null | Comparison estimate: SDK-reported for Claude; LiteLLM-estimated for Codex when usage is present and `model` is explicit and priced | | `duration_ms` | int | Total execution time | | `session_id` | str | Session ID (use with `resume` parameter to continue conversations) | | `num_turns` | int | Number of conversation turns | | `reasoning_output_tokens` | int | Codex reasoning tokens (Codex backend only) | | `retries` | array | Usage records for superseded structured-output correction calls, when any | Access via `${node_id.llm_usage.field}`: ```markdown ### step_one Start an analysis session. - type: agent - backend: codex - prompt: Analyze the codebase structure ### step_two Continue the same session with follow-up work. - type: agent - backend: codex - prompt: Now refactor the issues you found - resume: ${step_one.llm_usage.session_id} ```` ### Shell command The `shell` node stores the resolved command that was actually executed, accessible as `${node_id.command}`. Useful for logging or debugging when the command is built dynamically from templates. ### HTTP response details The `http` node exposes `response_headers` (dict) and `response_time` (seconds as float) alongside the response body. Useful for handling pagination links, rate limit headers, or performance monitoring. Per-node cost is available internally as `llm_usage.cost_usd` (estimated from token pricing, `null` for unknown models). Access it via a [code node's](/reference/nodes/code) `inputs` dict — it's not exposed as a template variable because pricing coverage varies by model. Aggregate cost across all nodes appears in the CLI JSON output after the workflow finishes. ## Escaping Literal `${...}` text (not a template variable) uses double dollar signs to escape: ````markdown theme={null} ### print_price Print the literal price variable. - type: shell ```shell command echo 'Price: $${PRICE}' ``` ```` This produces the literal string `Price: ${PRICE}` instead of trying to resolve a variable. ## Validation pflow validates template variables at workflow creation time: * **Unknown variables** → Error: "Unresolved variable: `${typo}`" * **Type mismatches** → Warning: "Expected string, got dict" * **Invalid syntax** → Error: "Invalid template: `${foo.}`" This works because node types declare their outputs — pflow knows at creation time what fields exist and what types they have. With arbitrary code, you'd discover these mismatches at runtime. Most validation happens when the workflow is created (compile-time). Some validations happen during execution (runtime): * **Compile-time**: Variable existence, type compatibility, syntax * **Runtime**: JSON parsing success, nested access on dynamic values If JSON auto-parsing fails at runtime, you'll see an "Unresolved variable" error. ## Related * [Nodes overview](/reference/nodes/index) - How nodes use template variables * [Batch processing](/how-it-works/batch-processing) - Item variables in batch nodes * [Debugging](/guides/debugging) - Troubleshooting template issues # Welcome to pflow Source: https://docs.pflow.run/index Your agent plans once, pflow handles the rest pflow is a workflow compiler for AI agents. Your agent reasons through a task once, pflow compiles it into a workflow, and that workflow runs instantly forever after — no repeated workflow generation, no repeated costs. Like shell scripts, but for AI operations. ## The problem AI agents re-reason through every task from scratch, even ones they've solved before: * **Inconsistency**: Agents take different paths or skip steps between runs — there's no way to verify without watching * **Cost**: Each reasoning pass costs tokens — the same tokens, for the same logic, every time * **Context bloat**: Loading tool schemas (especially MCP servers) consumes tokens before any work begins ## How pflow helps pflow separates workflow authoring from execution: 1. **Your agent designs the workflow once** - figures out what nodes to use and how to connect them 2. **pflow compiles the workflow** - saves it as a reusable `.pflow.md` file 3. **Execution is instant** - run the same workflow with different inputs, zero reasoning cost Nodes are individual tools (call an LLM or API, read a file, or any MCP tool). Workflows chain them together. Save a workflow and your agent can discover it later, reuse it with different inputs, or chain it into a larger workflow. ## Built for agents The workflow format is markdown — headings, YAML, and code blocks. Agents already think in this structure, so they can write and iterate on workflows using patterns they already know. A side effect: open a `.pflow.md` file on GitHub and it reads like documentation. Render it in your IDE and it's self-explanatory. The same file that executes as a workflow also serves as its own docs. ## From idea to command ```bash theme={null} # Your agent builds and iterates on a .pflow.md workflow file claude "Can you help me make a release workflow? Let's use pflow" # You or your agent save it for easy reuse pflow save ./workflow.pflow.md --name generate-changelog # Anyone can run it pflow generate-changelog since_tag=v0.7.0 # Next time, your agent already knows how claude "Generate a changelog for the next release" ``` ## Get started Install pflow and connect your AI tool Set up Claude Code, Cursor, VS Code, and more Expand pflow with external tools All commands documented # Claude Code Source: https://docs.pflow.run/integrations/claude-code Set up pflow with Claude Code **Prerequisites:** [Install pflow](/quickstart) before continuing. Claude Code has full terminal access, so pflow works out of the box — either through CLI commands or as an MCP server. ## Option 1: CLI access (recommended) Since Claude Code has terminal access, the simplest approach is to tell it to use pflow directly. Add to your project's `CLAUDE.md` or system instructions: ```markdown theme={null} Use pflow for workflow automation. Run `pflow guide` to learn the commands. ``` Claude Code will then run pflow commands directly when it needs to build or run workflows. ## Option 2: MCP server You can also add pflow as an MCP server for structured tool access. Run this command in Claude Code: ```bash theme={null} claude mcp add pflow -- pflow mcp serve ``` Or add manually to your MCP config: ```bash theme={null} claude mcp add pflow --command pflow --args "mcp,serve" ``` Check that pflow is configured: ```bash theme={null} claude mcp list ``` You should see `pflow` in the list of configured servers. ## Which to choose **CLI access** is simpler and recommended for most users. Claude Code can run any pflow command directly. **MCP server** is useful if you want Claude Code to have structured tools for workflow operations, or if you're using pflow with other MCP servers. Both approaches give Claude Code the same capabilities - discovering workflows, running them, and building new ones. ## Publishing workflows as skills You can publish saved workflows as Claude Code skills, making them discoverable without explicit instructions: ```bash theme={null} pflow skill save my-workflow ``` This creates a symlink in `.claude/skills/my-workflow/SKILL.md` pointing to your workflow. Claude Code automatically discovers skills in this directory. For personal skills available across all projects: ```bash theme={null} pflow skill save my-workflow --personal ``` See [pflow skill](/reference/cli/skill) for full documentation. # Claude Desktop Source: https://docs.pflow.run/integrations/claude-desktop Set up pflow with Claude Desktop **Prerequisites:** [Install pflow](/quickstart) before continuing. Claude Desktop connects to pflow via MCP server — giving Claude access to workflow discovery and the ability to run workflows directly. pflow is currently verified to work on macOS. Windows and Linux support is coming soon. ## Setup The config file is located at: ``` ~/Library/Application Support/Claude/claude_desktop_config.json ``` You can also access it through Claude Desktop: **Settings** → **Developer** → **Edit Config**. Add the pflow server to your `mcpServers` object: ```json theme={null} { "mcpServers": { "pflow": { "command": "pflow", "args": ["mcp", "serve"] } } } ``` If you have other MCP servers configured, add pflow alongside them: ```json theme={null} { "mcpServers": { "existing-server": { "command": "...", "args": ["..."] }, "pflow": { "command": "pflow", "args": ["mcp", "serve"] } } } ``` Completely quit Claude Desktop and restart it. The app needs to restart to load the new MCP server. Start a new conversation and ask Claude to list available pflow workflows: ``` Use pflow to list available workflows ``` Claude should be able to access pflow's tools and respond with workflow information. ## Troubleshooting 1. Make sure pflow is installed and in your PATH: `pflow --version` 2. Check your JSON syntax is valid (use a JSON validator) 3. Completely quit and restart Claude Desktop (not just close the window) Open Claude Desktop, go to **Settings** → **Developer** → **Edit Config**. Claude will create the file for you. # Cursor Source: https://docs.pflow.run/integrations/cursor Set up pflow with Cursor **Prerequisites:** [Install pflow](/quickstart) before continuing. Cursor supports both CLI access and MCP servers. The easiest way to get started is with one-click install. ## One-click install Click the button below to add pflow to Cursor: Install pflow in Cursor This adds pflow as an MCP server to your global Cursor config (`~/.cursor/mcp.json`). ## Manual setup If you prefer to set up manually, you have two options: **Global config** (all projects): `~/.cursor/mcp.json` **Project config** (single project): `.cursor/mcp.json` ```json theme={null} { "mcpServers": { "pflow": { "command": "pflow", "args": ["mcp", "serve"] } } } ``` You can also add via Cursor settings: **File** → **Preferences** → **Cursor Settings** → **MCP**. Open Cursor settings (**File** → **Preferences** → **Cursor Settings** → **MCP**) and check that pflow appears in the list of configured servers. Or ask Cursor to list pflow workflows: ``` Use pflow to list available workflows ``` Since Cursor has terminal access, you can also use pflow via CLI. Add to your project's instructions or tell Cursor: ``` Use pflow for workflow automation. Run `pflow guide` to learn the commands. ``` ## Troubleshooting The deeplink requires Cursor to be installed and registered as a protocol handler. Try manual setup instead. 1. Make sure pflow is installed and in your PATH: `pflow --version` 2. Check your MCP config JSON syntax is valid 3. Restart Cursor ## Publishing workflows as skills You can publish saved workflows as Cursor skills: ```bash theme={null} pflow skill save my-workflow --cursor ``` This creates a symlink in `.cursor/skills/my-workflow/SKILL.md` pointing to your workflow. For personal skills available across all projects: ```bash theme={null} pflow skill save my-workflow --cursor --personal ``` See [pflow skill](/reference/cli/skill) for full documentation. # AI tool integration Source: https://docs.pflow.run/integrations/overview Connect pflow to your AI coding assistant **Prerequisites:** Before you begin, [install pflow](/quickstart) and set up your API key. pflow works with any AI tool that has terminal access or supports MCP servers. Choose your tool below for setup instructions. ## Choose your AI tool Anthropic's CLI agent with terminal access Claude's desktop app (macOS) AI-powered code editor Visual Studio Code with GitHub Copilot Codeium's AI IDE ## Two ways to connect ### CLI access If your AI tool has terminal access (Claude Code, Cursor, Windsurf), the simplest approach is CLI access. Your agent runs pflow commands directly: ```bash theme={null} pflow guide ``` This gives the agent everything it needs to discover workflows, run them, and build new ones. ### MCP server For tools that support the Model Context Protocol, you can add pflow as an MCP server. This gives your agent structured tools for workflow operations. The MCP config for pflow is the same across all tools: ```json theme={null} { "mcpServers": { "pflow": { "command": "pflow", "args": ["mcp", "serve"] } } } ``` ### Same capabilities, different interface Both methods give your agent the same functionality - the MCP server mirrors the CLI commands with minor adjustments for structured tool use. Your agent can discover nodes, run workflows, save new ones, and manage settings either way. Choose based on what your AI tool supports. If it has terminal access, CLI is simpler. If it only supports MCP, use that. If it supports both, either works. ## Comparison | Tool | CLI access | MCP server | One-click install | | ----------------- | ---------- | ---------- | ----------------- | | Claude Code | Yes | Yes | - | | Claude Desktop | No | Yes | No | | Cursor | Yes | Yes | Yes | | VS Code / Copilot | Yes | Yes | Yes | | Windsurf | Yes | Yes | No | pflow is currently verified to work on macOS. Windows and Linux support is coming soon. # VS Code / Copilot Source: https://docs.pflow.run/integrations/vscode Set up pflow with Visual Studio Code and GitHub Copilot **Prerequisites:** [Install pflow](/quickstart) before continuing. VS Code with GitHub Copilot supports MCP servers. You can add pflow with one-click install or manual setup. ## One-click install Click the button below to add pflow to VS Code: Install pflow in VS Code If the button doesn't work, use the web redirect: Install via vscode.dev ## Manual setup **User config** (all projects): `~/.vscode/mcp.json` **Workspace config** (single project): `.vscode/mcp.json` ```json theme={null} { "mcpServers": { "pflow": { "type": "stdio", "command": "pflow", "args": ["mcp", "serve"] } } } ``` Reload the window or restart VS Code to load the new MCP server. ## Verify installation Ask Copilot to list pflow workflows: ``` Use pflow to list available workflows ``` Copilot should be able to access pflow's tools and respond with workflow information. ## Troubleshooting 1. Make sure VS Code is installed and the `vscode:` protocol handler is registered 2. Try the web redirect link instead 3. Fall back to manual setup 1. Make sure pflow is installed and in your PATH: `pflow --version` 2. Check your MCP config JSON syntax is valid 3. Reload VS Code window # Windsurf Source: https://docs.pflow.run/integrations/windsurf Set up pflow with Windsurf **Prerequisites:** [Install pflow](/quickstart) before continuing. Windsurf supports both CLI access and MCP servers. Since Windsurf has terminal access, you can use either approach. ## Option 1: CLI access (simplest) Windsurf has terminal access, so the simplest approach is to use pflow via CLI. Tell Windsurf: ``` Use pflow for workflow automation. Run `pflow guide` to learn the commands. ``` Windsurf will then run pflow commands directly when it needs to build or run workflows. ## Option 2: MCP server You can also add pflow as an MCP server for structured tool access. The config file is located at: ``` ~/.codeium/windsurf/mcp_config.json ``` Create the file if it doesn't exist. ```json theme={null} { "mcpServers": { "pflow": { "command": "pflow", "args": ["mcp", "serve"] } } } ``` Restart Windsurf to load the new MCP server. ## Verify installation Ask Windsurf to list pflow workflows: ``` Use pflow to list available workflows ``` Windsurf should be able to access pflow's tools and respond with workflow information. ## Troubleshooting 1. Make sure pflow is installed and in your PATH: `pflow --version` 2. Check your MCP config JSON syntax is valid 3. Make sure the config file is at `~/.codeium/windsurf/mcp_config.json` 4. Restart Windsurf The config path `~/.codeium/windsurf/mcp_config.json` is for macOS/Linux. On Windows, check Windsurf documentation for the equivalent path. # Quickstart Source: https://docs.pflow.run/quickstart Install pflow and set it up for your AI agent Your AI agent (Claude Code, Cursor, Windsurf) uses pflow to build and run workflows. You install it, configure an API key, and your agent handles the rest. **Prerequisites:** Python 3.10+ and [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/). On Windows, also install [Git for Windows](https://gitforwindows.org) — shell steps run through Git Bash so workflows use the same POSIX shell dialect on every platform (set `PFLOW_BASH` to point at a specific `bash.exe` if yours isn't auto-detected). ```bash theme={null} uv tool install pflow-cli ``` ```bash theme={null} pipx install pflow-cli ``` Verify installation: ```bash theme={null} pflow --version ``` pflow uses an LLM for **discovery** — finding the right workflows and nodes without your agent needing to load everything into context. You can use any provider supported by [LiteLLM](https://docs.litellm.ai/docs/providers) (OpenAI, Anthropic, Google, OpenRouter, Ollama, and 100+ more). ```bash theme={null} # OpenAI pflow settings set-env OPENAI_API_KEY "sk-..." # Or Anthropic pflow settings set-env ANTHROPIC_API_KEY "sk-ant-..." # Or Google pflow settings set-env GEMINI_API_KEY "..." ``` pflow auto-detects your provider and selects a default model (Claude Sonnet 4.5 for Anthropic, Gemini 3 Flash for Google, GPT-5.2 for OpenAI). Shell environment variables work too — anything `export ANTHROPIC_API_KEY=...` exports is picked up automatically. To use a different model than the auto-detected default: ```bash theme={null} pflow settings llm set-default anthropic/claude-sonnet-4-5 ``` See [LLM model settings](/reference/cli/settings#llm-model-settings) for all options. This is **pflow's LLM configuration**, separate from whatever LLM your agent uses: * **Discovery commands** - `pflow mcp find` and `pflow find` use LLM to find relevant nodes and workflows * **LLM nodes** - workflows that include an LLM node for text processing * **Smart filtering** - automatic field selection for large API responses Your agent creates workflows using its own LLM (Claude Code uses Claude, Cursor uses its models, etc.). pflow's model configuration is only for pflow's internal features. Set up an LLM if you're using MCP servers. MCP tool descriptions can consume a third of your agent's context before any work starts — discovery is how pflow avoids that. For basic usage without MCP servers or testing pflow, the configuration is optional. Discovery costs a fraction of what your agent spends per task. By loading only what's relevant, it cuts token usage, speeds up responses, and keeps context focused. Your AI agent can use pflow in two ways: **Option 1: CLI access (easiest)** If your agent has terminal access (Claude Code, Cursor, Windsurf), instruct it to run: ```bash theme={null} pflow guide ``` This gives the agent everything it needs to discover existing workflows, run them, or build new ones. **Option 2: MCP server** Add pflow to your AI tool's MCP config (Claude Desktop, Cursor, etc.): ```json theme={null} { "mcpServers": { "pflow": { "command": "pflow", "args": ["mcp", "serve"] } } } ``` See [AI tool integration](/integrations/overview) for detailed setup instructions for each tool. ## What your agent can do with pflow Once connected, your agent can: * **Discover workflows**: `pflow find "what I want to do"` * **Run workflows**: `pflow my-workflow param1=value1` * **List workflows**: `pflow list` * **Discover nodes**: `pflow mcp find "capability I need"` * **Build new workflows**: Following `pflow guide` ## Next steps Set up Claude Desktop, Cursor, or other AI tools Expand pflow capabilities with external tools ## Troubleshooting Make sure the install location is in your PATH: ```bash theme={null} # For uv echo $PATH | grep -q "$HOME/.local/bin" || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc # For pipx pipx ensurepath ``` Then restart your terminal. pflow requires Python 3.10+. Check your version: ```bash theme={null} python --version ``` If you need to upgrade, use [pyenv](https://github.com/pyenv/pyenv) or your system package manager. # pflow analyze-cache Source: https://docs.pflow.run/reference/cli/analyze-cache Find prompt caching opportunities and predict savings ## Usage ```bash theme={null} pflow analyze-cache [PARAMS...] pflow analyze-cache [PARAMS...] --format=json pflow analyze-cache --from-trace pflow analyze-cache --list-traces ``` `pflow analyze-cache` reads a workflow file (or saved workflow name), finds LLM calls that share static context, and emits recommendations: which values to add to a `## Cache` block, which nodes should opt in, and projected cost savings. It runs in three modes depending on what data it can find: | Mode | Triggers when | Output emphasis | | ---------------- | ----------------------------------------------- | ------------------------------------------------------------ | | **Greenfield** | Workflow has no `## Cache` block | Detection + paste-ready suggested block | | **Steady-state** | Workflow has `## Cache` declared | Per-chunk usage, validation, padding advisories | | **Trace-driven** | A 2.x trace was loaded (auto or `--from-trace`) | Predicted vs actual cache ratios with root-cause attribution | Inputs are optional. When omitted, token estimates fall back to lower-fidelity sources (memo cache → tokenizer → character heuristic) and the confidence label reflects that. Required inputs that aren't supplied surface as a single info note rather than blocking the analysis. ## Examples ```bash theme={null} # Analyze a workflow file (auto-loads matching trace from ~/.pflow/debug/) pflow analyze-cache ./song-creator.pflow.md # Skip auto-load, analyze fresh pflow analyze-cache ./song-creator.pflow.md --no-trace-autoload # Compare predicted to actual using an explicit trace pflow analyze-cache ./song-creator.pflow.md \ --from-trace ~/.pflow/debug/workflow-trace-abc12345-song-creator-20260507.json # List matching traces and the trace that auto-load would pick pflow analyze-cache ./song-creator.pflow.md --list-traces # JSON output for agent consumption pflow analyze-cache ./song-creator.pflow.md --format=json # Show every node, not just rows with warnings or low cache ratios pflow analyze-cache ./song-creator.pflow.md --all-rows ``` ## Options | Flag | Default | Description | | --------------------- | ------- | ------------------------------------------------------------------- | | `--format=text\|json` | `text` | Human-readable text or stable JSON for agents | | `--from-trace ` | - | Explicit trace file (any 2.x format). Overrides auto-load | | `--no-trace-autoload` | off | Skip the most-recent matching trace from `~/.pflow/debug/` | | `--all-rows` | off | Show every LLM node in the per-call table; default hides clean rows | | `--list-traces` | off | List matching traces and exit without running analysis | `--from-trace` and `--no-trace-autoload` are mutually exclusive. `--list-traces` is mutually exclusive with `--from-trace`, `--no-trace-autoload`, and `--all-rows`; use it as a discovery command, then run analysis with the chosen trace. ## Output Text output is organized into sections that appear when non-empty: | Section | What it shows | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Header | Workflow path, scale (LLM call count, models in use), confidence label | | Summary | Current cost per run, projected cost with caching, projected rerun cost (within TTL) | | Recommended actions | Numbered (ordered by impact when at least one action has a positive savings figure; unordered when no model is resolved or all savings are unavailable). Each item carries a stable warning ID and the edit to apply. | | Suggested ## Cache block | Paste-ready block for greenfield mode, with starter prose for each chunk | | Sub-workflow boundaries | Cross-file findings: rename detection, prose mismatches, value-flow opportunities | | Per-call cache report | Table of LLM nodes with model, input tokens, cached-now tokens, ready tokens, upside tokens, ratio, confidence | | Notes | Per-invocation scoping notes, mixed-model context, fallback hints | JSON output (`--format=json`) emits the same data with stable field names and `format_version` for consumer version-gating. See `pflow analyze_cache` MCP tool for the full schema. ## Confidence labels The header shows an aggregate confidence label based on what data was available: | Label | Meaning | | ------------------ | ------------------------------------------------------------- | | `high_from_trace` | Token counts read from a runtime trace — actual numbers | | `medium_from_memo` | Token counts from prior runs via the memo cache | | `low_no_data` | Token counts estimated from the prompt template via tokenizer | Per-row counts include their own `data_source` so you can tell which rows have real data vs estimates. ## Stable warning IDs Findings carry namespaced IDs (e.g., `cache.shared-context-undeclared`, `cache.batch-prewarm-recommended`, `cache.below-min-predicted`). The full catalog and what each ID means is in the [Prompt caching how-it-works](/how-it-works/prompt-caching) guide. ## Exit codes | Code | Meaning | | ---- | ----------------------------------------------------------------------- | | `0` | Analysis succeeded (warnings still surface in output) | | `1` | Workflow couldn't be parsed or resolved | | `2` | Invalid flag combination (e.g., `--from-trace` + `--no-trace-autoload`) | Warnings never make the command exit non-zero — they're advisory by design. An agent that wants to gate on findings inspects `warnings[].severity == "error"` in the JSON output. ## Related * [Prompt caching how-it-works](/how-it-works/prompt-caching) — what the `## Cache` block does and when to use it * [LLM node reference](/reference/nodes/llm) — `prompt_cache:` and `prewarm:` field documentation # pflow describe Source: https://docs.pflow.run/reference/cli/describe Show a saved workflow or local workflow file's interface ## Usage ```bash theme={null} pflow describe ``` Shows inputs, outputs, and a copyable example command. Saved workflows also include their execution history; unsaved files omit history. ```bash theme={null} pflow describe my-saved-workflow pflow describe ./drafts/release-notes.pflow.md pflow describe './draft workflows/release notes.pflow.md' ``` # pflow find Source: https://docs.pflow.run/reference/cli/find Find saved workflows by intent using an LLM ## Usage ```bash theme={null} pflow find "" ``` Use `pflow find` when you know what you want to do but not which saved workflow already matches it. # pflow guide Source: https://docs.pflow.run/reference/cli/guide Agent-oriented workflow building guidance ## Usage ```bash theme={null} pflow guide # Same as pflow --help pflow guide # Load specific topics pflow guide ./workflow.pflow.md # Auto-detect topics from a workflow pflow guide ./workflow.pflow.md batch # Auto-detect + explicit topic ``` Without arguments, `pflow guide` shows the same entry content as `pflow --help` — a capability map with commands and available topics. With topics, it returns tailored content for building workflows using those specific nodes or features. Topics and workflow references can mix freely. ## Topics **Core:** * `core` — Framework fundamentals: step order vs templates, input declaration, node selection, development loop **Nodes:** * `http` — HTTP requests to REST APIs * `llm` — LLM inference with structured output * `code` — Python data transformation * `shell` — Shell commands and CLI tools * `file` — File read/write * `mcp` — MCP service integrations **Features:** * `batch` — Same operation on multiple items (parallel/concurrent) * `branching` — Conditional paths via on-error or code routing * `sub-workflows` — Reusable sub-workflow composition ## Workflow-scoped mode When given a workflow file path or saved workflow name, `pflow guide` parses the workflow and auto-loads guide content for every node type and feature it uses: ```bash theme={null} # Auto-detects: http + code topics (because the workflow uses those node types) pflow guide ./my-api-pipeline.pflow.md # Auto-detect + add batch topic explicitly pflow guide ./my-api-pipeline.pflow.md batch ``` This is useful when handed an unfamiliar workflow — run `pflow guide ./it.pflow.md` to get exactly the content needed to understand it. ## Design `core` is an explicit topic, not auto-included. Agents load it once at the start of a session, then load node/feature topics as needed without duplicating framework fundamentals. Guide prose lives in static markdown files under `src/pflow/guide/`. Node topics additionally include a dynamic Parameters/Outputs section loaded from the registry at render time, keeping interface documentation in sync with actual node implementations. # pflow history Source: https://docs.pflow.run/reference/cli/history Show execution history for a saved workflow ## Usage ```bash theme={null} pflow history ``` Shows prior execution status and the last used inputs for the workflow. # CLI overview Source: https://docs.pflow.run/reference/cli/index Complete reference for pflow command-line interface pflow provides a CLI for running workflows, managing MCP servers, and configuring settings. **Who runs these commands?** Most pflow commands are run by your AI agent, not by you directly. You handle setup (installation, API keys, MCP servers), then your agent uses pflow to build and run workflows. This reference documents all commands so you understand what your agent is doing. See [Using pflow](/guides/using-pflow) for what to expect day-to-day. ## Command structure ```bash theme={null} pflow [command] [options] [arguments] ``` ## Command groups Run workflows by name or file Find saved workflows and inspect them by saved name or file path Publish workflows as AI agent skills Learn the surface and test single nodes Manage MCP server connections Configure API keys and node filtering Interactive browser canvas for a workflow Generate a Mermaid flowchart from a workflow Get AI agent entry guidance ## Main command The default `pflow` command runs workflows. Your agent uses this to run saved workflows or workflow files it has created. ### Run a saved workflow ```bash theme={null} pflow my-workflow input=data.txt threshold=0.5 ``` ### Run from a file ```bash theme={null} pflow ./workflow.pflow.md pflow ~/workflows/analysis.pflow.md param=value ``` **No built-in natural language mode.** pflow executes workflow files and saved workflows. Your AI agent builds workflows using pflow's MCP tools or CLI primitives — pflow doesn't have its own natural language interface. ## Resume a failed run When a run fails partway — an LLM/HTTP/MCP step times out, a transient error survives retries — `pflow resume` continues from the failed step instead of re-running the whole workflow. It restores the already-completed upstream steps' outputs from the saved trace (it does not re-run them) and walks forward from the failed step. ```bash theme={null} pflow resume 7a9e4afb-2095-447e-8043-0660438104f9 # resume that exact failed attempt pflow resume my-workflow # resume the newest failed run of a workflow pflow resume my-workflow api_url=https://fixed.example.com # override an input while resuming ``` The failed run prints the exact command to use (`To resume from the failed step: pflow resume `). A resumed run is a new run with its own trace, linked to the original. | Option | Description | | ----------- | -------------------------------------------------------------------------------------------- | | `--force` | Bypass the side-effect confirmation and the edited-workflow check | | `--dry-run` | Preview the resumed tail's cost and cache plan (from the failed step onward) without running | | `KEY=VALUE` | Override the original run's inputs | The failed step runs again from the start, so if it already partly side-effected (an HTTP POST that sent but timed out on the response), resuming re-fires it — at-least-once execution. A side-effecting step (`shell`/`code`/`agent`/file-ops/`mcp`) asks for confirmation at a terminal and refuses for non-interactive agents unless you pass `--force`; an idempotent `llm` step resumes silently. See [`pflow guide resume`](/reference/cli/guide) for the full behavior (loop restart, gate re-prompting, top-level granularity, interrupted-run handling). ## Answer a paused gate An [approval gate](/how-it-works/approval-gates) reached in a non-interactive run — a pipe, CI, an MCP call, a browser-launched run — doesn't fail and doesn't hang. The run **pauses**, saves its state to the trace, and exits with code **4** with a resume token on stdout: ``` Paused at 'notify-slack'. Resume token: 7a9e4afb-2095-447e-8043-0660438104f9 (exit 4) ``` The gate's content follows on stderr — an approval's resolved preview, or an escalation's question, options, and recommendation — so a calling process can compose the answer from the output alone. With `--output-format json`, the paused document carries `status: "paused"`, `execution_id`, `paused_node_id`, `gate_request`, and `resume_command`. Answer it later — hours or days on — and the run continues from the gated step without re-running anything upstream: ```bash theme={null} pflow resume 7a9e4afb-2095-447e-8043-0660438104f9 --approve yes # run the gated step and continue pflow resume 7a9e4afb-2095-447e-8043-0660438104f9 --approve no # deny cleanly (exit 3), no side effects pflow resume 7a9e4afb-2095-447e-8043-0660438104f9 --choose "per-env" # answer an escalation (or --choose 2) pflow resume list # pending pauses waiting for an answer ``` | Option | Description | | --------------------- | ----------------------------------------------------------------------------- | | `--approve yes\|no` | Answer an approval gate — run the gated step, or deny and end the run | | `--choose ""` | Answer an agent escalation — free text, or an option number from the question | | `resume list` | List pending unanswered pauses (token, workflow, gated step, gate kind, age) | Nothing upstream re-runs — completed steps are restored from the paused trace. An approval gate fires *before* its step, so an approved step runs for the first time (no confirmation prompt, no re-fired side effect); an answered escalation continues at the next step with the decision folded in, so the agent step is never re-paid. Answering consumes the token: the resumed attempt supersedes the paused run, and a second answer refuses and names the newer attempt. Resuming without an answer flag refuses and prints the pending question with the exact command. The edited-workflow refusal and `--force` apply the same as a failed resume. Durable pause needs the trace. A gate under `--no-trace`, inside a parallel batch item, in a sub-workflow child, or in an inline/piped workflow (no file to reload) can't pause — it fails at the gate instead. Save the workflow to a file and run it by name or path so its gates can pause. ## Global options | Option | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `--version` | Show pflow version | | `-v, --verbose` | Show detailed execution output | | `-o, --output-key KEY` | Specific shared store key to output | | `--output-format text\|json` | Output format (default: text) | | `-p, --print` | Minimal output: suppress header, summary, and warnings | | `--no-trace` | Disable workflow trace saving | | `--cache/--no-cache` | Enable/disable memoization cache reads (default: enabled) | | `--only NODE` | Re-run just this node against a snapshot of the last full run (upstream restored, not re-executed; needs a prior full run) | | `--auto-approve NODE` | Pre-approve one approval gate by step name (repeatable; no approve-all form). See [Approval gates](/how-it-works/approval-gates) | | `--validate-only` | Validate workflow without running | | `--dry-run` | Preview which nodes would run or serve from cache, without executing | | `--help` | Show help message | Older natural-language workflow generation flags have been removed. Use the current global options shown above, or let your AI agent build `.pflow.md` workflows directly. ## Parameter syntax Pass parameters to workflows using `key=value` syntax: ```bash theme={null} pflow my-workflow input=data.txt count=10 enabled=true ``` **Type inference:** * `true` / `false` → boolean * `10` → integer * `3.14` → float * `'["a","b"]'` → JSON array * `'{"key":"val"}'` → JSON object * Everything else → string ## Stdin input Pipe data into workflows that declare an input with `stdin: true`: ```bash theme={null} echo "test content" | pflow my-workflow cat data.csv | pflow csv-analyzer ``` The workflow must have an input marked to receive stdin: ```markdown theme={null} ## Inputs ### data Data input from stdin pipe. - type: string - required: true - stdin: true ``` Piped data routes to this input automatically. CLI parameters override stdin if both are provided: ```bash theme={null} # CLI parameter wins - "override" is used, not piped content echo "piped" | pflow my-workflow data="override" ``` For workflow chaining, use the `-p` flag to output results for the next workflow: ```bash theme={null} pflow -p step1 | pflow -p step2 | pflow step3 ``` ## Stdout output Workflows that declare multiple outputs mark one with `stdout: true` to pick which output lands on process stdout in text mode: ```markdown theme={null} ## Outputs ### message Primary result — streams to stdout on redirect or pipe. - source: ${emit.stdout} - stdout: true ### length Secondary metadata. Available via `-o length` or `--output-format json`. - source: ${count.stdout} ``` Redirecting or piping the CLI in text mode now writes only the marked output to stdout: ```bash theme={null} pflow stdout-result.pflow.md > result.txt # file contains the message only pflow stdout-result.pflow.md | next-step # message streams to the next command pflow stdout-result.pflow.md -o length # override: emit length instead pflow stdout-result.pflow.md --output-format json # emit all outputs as structured JSON ``` Single-output workflows don't need the marker — their one output is unambiguous. Workflows with multiple declared outputs and no `stdout: true` stream the first declared output and print a warning on stderr naming the other outputs and the three ways to change the routing: add the marker, pass `-o`, or switch to JSON mode. The validator enforces that at most one output per workflow is marked. ## Output modes ### Text mode (default) Human-readable output with live progress streamed to stderr and results on stdout. Works the same way in a terminal, CI log, agent bash tool, or subprocess capture: ```bash theme={null} pflow workflow.pflow.md ``` Progress lines and execution summary go to stderr. Declared workflow outputs go to stdout. ### JSON mode Structured output on stdout for machine parsing: ```bash theme={null} pflow --output-format json workflow.pflow.md ``` All workflow results, metrics, and errors serialize to a single JSON object on stdout. Progress and execution summary go to stderr (suppressed with `-p`). **Success response:** ```json theme={null} { "success": true, "result": { "summary": "Analysis complete", "count": 42 }, "duration_ms": 456.78, "total_cost_usd": 0.02, "nodes_executed": 3, "execution": { "duration_ms": 456.78, "nodes_executed": 3, "nodes_total": 3, "cache_hits": 2, "steps": [ { "node_id": "fetch-data", "status": "completed", "duration_ms": 0, "cached": true }, { "node_id": "read-data", "status": "completed", "duration_ms": 50.12, "cached": false } ] }, "metrics": { "workflow": { "duration_ms": 456.78, "nodes_executed": 3, "nodes_cached": 0 }, "total": { "duration_ms": 456.78, "total_cost_usd": 0.02, "total_tokens": 1500, "llm_calls": 1 } } } ``` **Error response:** ```json theme={null} { "success": false, "error": "Workflow failed with action: error", "errors": [ { "source": "runtime", "category": "api_validation", "message": "Missing required parameter: 'repo'", "node_id": "fetch-issues", "fixable": true, "available_fields": ["user_input", "stdin"] } ], "execution": { "steps": [ {"node_id": "fetch-issues", "status": "failed", "duration_ms": 120.5} ] } } ``` **Key fields:** | Field | Description | | --------------------------- | -------------------------------------------------------------------- | | `success` | `true` if workflow completed, `false` if failed | | `result` | Workflow output (object, string, or array based on declared outputs) | | `duration_ms` | Total execution time in milliseconds | | `total_cost_usd` | LLM API cost (0.0 if no LLM calls) | | `execution.cache_hits` | Number of nodes served from memoization cache | | `errors[].available_fields` | Valid fields for template variables - helps agents fix errors | ### Print mode (`-p`) Minimal stderr output when you want the cleanest possible data stream: ```bash theme={null} pflow -p workflow.pflow.md | jq '.data' ``` Suppresses the "Workflow output:" header, the execution summary, and stderr warnings. Data still goes to stdout (same as default mode). Useful for piping into tools that should only see the result. ## Exit codes | Code | Meaning | | ----- | -------------------------------------------------------------------------------------- | | `0` | Workflow completed, including runs that completed with warnings (`status: "degraded"`) | | `1` | Workflow failed | | `130` | Workflow interrupted | Runtime warnings remain visible in stderr, JSON output, traces, and reports; they do not make a completed workflow a process failure. ## Validation mode Validate a workflow without running it: ```bash theme={null} pflow --validate-only workflow.pflow.md pflow --validate-only my-saved-workflow ``` Agents use this to check workflows before running them — pflow catches template errors, type mismatches, and missing inputs during validation, so problems surface immediately instead of after step 5 fails. Exit code 0 means valid. ## Dry-run mode Preview what a workflow would do without running it. `--dry-run` walks the graph using the same cache lookup the engine uses at runtime, but never invokes a node — no shell commands, LLM calls, HTTP requests, file writes, or trace files: ```bash theme={null} pflow ./workflow.pflow.md --dry-run topic=hello ``` Cached nodes render with `↻`, would-execute nodes with `▸`, and a divider marks the cache boundary: ``` Dry-run for workflow.pflow.md: 2 nodes ↻ fetch (1m ago) ─── cache boundary: 'summarize' ─── ▸ summarize [code] Summary: 1 cached · 1 would execute (1 code) Estimated duration: ~1ms (historical, actual may vary) ``` When everything is cached, there's no boundary. When nothing is cached, the divider reads `nothing cached — full run`. For would-execute LLM nodes, the plan surfaces the cost from the most recent cache entry — labeled `≈` because pricing may have drifted. Per-node duration annotations appear on any would-execute node whose last run took at least 1 second; faster nodes stay bare: ``` ▸ summarize [LLM] ≈ $0.02 (last run 15m ago) ▸ upload [shell] ~1.5s (last run 2m ago) ``` ### JSON output ```bash theme={null} pflow ./workflow.pflow.md --dry-run --output-format json topic=hello ``` Top-level shape: `{workflow, plan, summary, diagnostics}`. **Summary fields agents use for cost gating:** | Field | Description | | ----------------------- | ----------------------------------------------------------------------------------------------------- | | `estimated_cost_usd` | Sum of historical LLM costs across would-execute nodes | | `estimated_duration_ms` | Sum of historical durations across would-execute nodes | | `cost_basis` | `"exact"` for linear plans; `"upper_bound"` when branches are enumerated | | `cache_boundary` | Node ID of the first cache miss, or `null` when everything is cached | | `execute_by_type` | Count of would-execute nodes by type: `{"LLMNode": 1, "ShellNode": 2}` | | `nodes_without_history` | Would-execute LLM nodes missing cost data — non-zero means the estimate is incomplete | | `opaque_count` | Sub-workflows the planner couldn't resolve (e.g. `workflow: ${dynamic-ref}`) — their cost is excluded | **Plan entry fields:** `node_id`, `node_type`, `status` (`cached`, `execute`, `sub_workflow`, `opaque`, `routing_error`), `cause`, `last_cost_usd`, `last_duration_ms`, `age_sec`. Agents cost-gating on `estimated_cost_usd` should check `opaque_count == 0` and `nodes_without_history == 0` first — otherwise the estimate is missing data. ### Flag combinations | Flag | With `--dry-run` | | -------------------------- | ------------------------------------------------------------------------------- | | `--validate-only` | Rejected with exit 1 — different audiences, different exit contracts | | `--report`, `--report-dir` | Rejected with exit 1 — no execution means no report | | `--no-cache` | Every node shows as would-execute | | `--only NODE` | Plan stops at the named node | | `--no-trace`, `-p`, `-o` | Accepted silently — dry-run writes no traces, and the plan itself is the result | Exit 0 on a successful plan, 1 on planner-level failures (missing input, compile error, unresolvable sub-workflow, cycle, max depth exceeded). ## Iteration and caching pflow caches node outputs automatically. When your agent re-runs a workflow file, unchanged nodes return instantly from a persistent cache — only nodes whose configuration or inputs changed will re-execute. ```bash theme={null} # First run: all nodes execute pflow ./workflow.pflow.md title=hello # Second run (same inputs): all nodes served from cache pflow ./workflow.pflow.md title=hello # Changed input: only affected nodes re-execute pflow ./workflow.pflow.md title=different ``` The cache is content-addressed — same node config plus same resolved inputs produces the same cache key, regardless of when or how the workflow was run. Cache entries expire after 24 hours. The cache lives at `~/.pflow/cache/cache.db`. ### Run a single node The `--only` flag runs just the named node against a frozen snapshot of the most recent full run. Every other node's output is restored from that run — not re-executed — so a side-effecting upstream node like `shell: git push` does **not** run again. It needs a prior full run to snapshot from (otherwise it errors). Targeting a node inside a sub-workflow is not supported. ```bash theme={null} pflow ./workflow.pflow.md # full run once (records the snapshot) pflow ./workflow.pflow.md --only process-data # re-run just this node ``` Without `-o`, `--only` streams the targeted node's result to stdout instead of the workflow's full-run outputs. Pass `-o ` when you need a specific named output. This is how agents iterate on a specific node without re-running the full workflow. ### Bypass memo cache reads Use `--no-cache` to bypass pflow memo-cache reads, so nodes execute again. Memo cache writes still happen, so the next run without `--no-cache` benefits from the results: ```bash theme={null} pflow ./workflow.pflow.md --no-cache ``` Use this when a node has external side effects (API calls, file writes) that should run again, or when memoized results seem stale. It does not disable LLM provider prompt caching declared with `## Cache` / `prompt_cache:`, OpenAI automatic prompt caching, or Gemini implicit caching. ### Per-node caching Only `llm` nodes cache by default — their output is purely a function of their declared inputs. Every other node type (`shell`, `code`, `http`, file ops, `mcp`, `agent`) defaults to NOT caching, because they side-effect or read external state (`git branch`, `date`, environment variables, files, network). So a node like `git branch --show-current` re-runs every time with no annotation needed: ````markdown theme={null} ### get-branch Detect the current git branch. - type: shell ```shell command git branch --show-current ```` ```` Use `- cache: true` to opt a node INTO caching when its output is purely a function of its declared inputs (no filesystem reads, no clock, no env vars, no network state). Most shell/code/http/file/mcp nodes do not qualify. `- cache: false` is the explicit opt-out — redundant for non-`llm` nodes under the default, but useful for documenting intent. Both skip cache reads and writes for that node, unlike `--no-cache` which is run-wide. ## Traces and reports By default, pflow saves execution traces to `~/.pflow/debug/`: - **Workflow traces**: `workflow-trace-{name}-{timestamp}.json` Generate a structured execution report (one markdown file per node): ```bash # During execution pflow my-workflow --report # Custom output directory pflow my-workflow --report-dir ./my-report/ # From an existing trace (most recent) pflow report # From a specific trace pflow report ~/.pflow/debug/workflow-trace-my-workflow-20260323-160000.json ```` Reports include rendered prompts, responses, cost data, error summaries with fix suggestions, and anomaly warnings. Report output directories are replaced as generated snapshots. Custom report directories must be empty or already contain pflow's `.pflow-report.json` marker. Disable traces with `--no-trace` for faster execution (the `--report` flag overrides `--no-trace`). ## UI command Serve an interactive browser canvas for a workflow — the visual counterpart to [`pflow mermaid`](#mermaid-command). Where `mermaid` emits static text, `pflow ui` opens a local browser canvas: collapsible sub-workflow, batch, and loop containers, click-to-read prompts and params, `${ref}` data-flow lines, and a source pane. ```bash theme={null} pflow ui workflow.pflow.md # open straight to a workflow pflow ui my-saved-workflow # a saved workflow by name pflow ui # the catalog of saved workflows pflow ui focus my-saved-workflow fetch-data pflow ui user-activity my-saved-workflow ``` The canvas updates in place — no page reload — as the workflow's `.pflow.md` is edited on disk, so you can watch it take shape (while keeping the current zoom and selection) as you, or an AI agent, build it. An edit that doesn't validate is held: the last valid version stays on screen with an error banner until the source parses again. The server blocks until `Ctrl+C`, so run it in the background while you edit. The canvas also shows runs in progress. Run a workflow in another terminal — or have your agent run it — and the open canvas lights each node as it starts and finishes: running, succeeded, cached, failed, or stopped, with a hover chip showing that node's duration and cost. It works the same for a finished run: open the run selector to replay any past run on the graph, or click a node to see what that run produced — its resolved inputs, output, cost, and tokens. With no workflow argument, the catalog marks which workflows are running now and when each last ran. Runs on a standard install. If `pflow ui` reports a missing dependency, add the server extra: `uv tool install 'pflow-cli[ui]'`. | Option | Description | | ------------------ | ----------------------------------------------------------- | | `--port N` | Port to serve on (default: 8765) | | `--no-open` | Don't open a browser window | | `--no-auto-update` | Freeze the view — don't live-update when the source changes | The full view is described by the URL, so you can share or screenshot an exact state — for example `?workflow=&focus=&direction=TD`. `focus=` highlights a node and reveals its connections; `node=` centers the camera on it. An agent can interact with every open Viewer showing a workflow: ```bash theme={null} pflow ui focus [--open] [--say TEXT] [--no-wait] # focus and reveal a step, input/output, or connection pflow ui frame [--say TEXT] [--no-wait] # move the camera without changing focus pflow ui clear-focus pflow ui user-activity [workflow] # recent deliberate clicks and view changes ``` Name a target the way it reads in the `.pflow.md`: a step, input, or output by its name (`process_content`, `source_file`), or a connection as `source -> target` (`gen.response -> summarize.prompt`) — there's no separate notation to learn. If a name matches more than one thing — the same step inside two sub-workflows, or an input and output that share a name — the command lists qualified addresses to pick from instead of guessing, and an unknown name returns the closest real matches. So you point, read the reply, and re-point. `--say "text"` narrates the point aloud in every open Viewer with a persistent on-canvas caption (text-to-speech via the configured `tts_model`/`tts_voice`; needs a Gemini API key). Delivery direction goes in `[brackets]` — e.g. `--say "[excited] this is the LLM call"` — bracketed tags shape the voice and are stripped from the caption; everything outside brackets is spoken and shown. If synthesis fails (no key, network), the point and caption still land and the reason appears in the report as `narration unavailable: ...`. A `--say` command waits for the previous clip to finish before it points, so running several in a row produces a self-pacing narrated walkthrough with no dead air — the next line synthesizes while the current clip is still playing, and each command returns as soon as its own clip starts. `--no-wait` points immediately instead (the new clip interrupts the playing one). If the browser blocks autoplay (a fresh window before any click), the next `--say` holds the walkthrough — reporting `note: narration is blocked in the Viewer…` — until a click on the caption's ▶ button plays the blocked line and unlocks sound; after \~2 minutes it continues with captions only. Each narrated target keeps its caption box on the canvas with a Replay button once the clip finishes; a new `--say` to the same target replaces that box, and `clear-focus` closes them all and stops the voice. These commands target port 8765 by default (pass `--port N` for another instance). Point commands exit nonzero when the target is unresolved/ambiguous or no Viewer received the command. `focus --open` opens a Viewer only when no window for that workflow is connected. ## Mermaid command Generate a [Mermaid](https://mermaid.js.org/) flowchart from a workflow. Shows the graph topology — nodes, edges, conditional branches, error routes, inputs, outputs — that's otherwise scattered across individual node directives in the `.pflow.md` file. For an interactive, collapsible canvas instead of static text, see the [UI command](#ui-command). ```bash theme={null} pflow mermaid workflow.pflow.md pflow mermaid my-saved-workflow ``` The command validates the workflow first (same checks as `--validate-only`). On validation failure, it shows diagnostics and exits with code 1. On success, it outputs Mermaid syntax to stdout. ```bash theme={null} # Save to file (raw Mermaid syntax) pflow mermaid workflow.pflow.md -o diagram.mmd # Save as markdown with title, optional description, and fenced mermaid block pflow mermaid workflow.pflow.md -o diagram.md # Or pipe to clipboard pflow mermaid workflow.pflow.md | pbcopy ``` Mermaid renders natively in GitHub, VS Code, and most markdown viewers — no extra tooling needed. The `.md` output wraps the diagram in a markdown document with the workflow's title and description, ready to commit or share. | Option | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `-o, --output FILE` | Write to file. `.md` extension wraps in markdown with title and fenced code block. Any other extension writes raw Mermaid syntax. | | `--depth N` | Sub-workflow expansion depth (default: 5, 0 = no expansion) | | `--direction LR\|TD` | Graph direction: left-to-right or top-down (default: LR) | | `--descriptions` | Add first sentence of each node's purpose to its label | Sub-workflow nodes (`type: workflow`) expand into `subgraph` blocks showing their internal structure. Use `--depth 0` to render them as opaque nodes, or `--depth 2` to expand nested sub-workflows: ```bash theme={null} # No sub-workflow expansion pflow mermaid workflow.pflow.md --depth 0 # Expand two levels deep, top-down layout pflow mermaid workflow.pflow.md --depth 2 --direction TD # Include node descriptions in labels pflow mermaid workflow.pflow.md --descriptions ``` For a workflow with conditional branching: ```mermaid theme={null} graph LR classDef code fill:#D5E8D4,stroke:#82B366,color:#000 classDef llm fill:#E8D5F5,stroke:#7B2D8E,color:#000 classDef shell fill:#DAE8FC,stroke:#6C8EBF,color:#000 classDef mcp fill:#FFE6CC,stroke:#D79B00,color:#000 classDef writefile fill:#F8CECC,stroke:#B85450,color:#000 classDef workflow fill:#FFF2CC,stroke:#D6B656,color:#000 classDef decision fill:#F5F5F5,stroke:#666666,color:#000 classDef input fill:#F5F5F5,stroke:#666666,stroke-dasharray:5 5,color:#000 classDef output fill:#E8E8E8,stroke:#666666,color:#000 fetch-data[["fetch-data (shell)"]]:::shell classify{"classify (code)"}:::decision process-small[["process-small (shell)"]]:::shell process-large[["process-large (shell)"]]:::shell handle-error[["handle-error (shell)"]]:::shell done[["done (shell)"]]:::shell pflow_end(("end")) fetch-data --> classify classify -.->|error| handle-error classify -->|process-large| process-large classify -->|process-small| process-small process-small --> done process-large --> done done --> pflow_end handle-error --> pflow_end ``` Node shapes indicate type: `[["shell"]]` rectangles for shell, `{"code"}` diamonds for decision nodes, `(["output"])` stadiums for outputs. Edge styles: `-->` for normal flow, `-->|action|` for named branches, `-.->|error|` for error routes. Workflow inputs and outputs render as dashed-border groups. Batch nodes show as subgraphs with their items. Sub-workflows expand into nested subgraphs with input/output wrappers showing data flow across boundaries. ## Guide command The `pflow guide` command provides the entry guidance for AI agents using pflow. ```bash theme={null} pflow guide pflow guide http llm ``` Without topics it renders the same entry content as `pflow --help`. Topic composition is introduced in Task 77. ## Related * [Workflow commands](/reference/cli/list) - Find and manage saved workflows * [Skill commands](/reference/cli/skill) - Publish workflows as AI agent skills * [Guide and probe](/reference/cli/guide) - Learn the surface and test single nodes * [MCP commands](/reference/cli/mcp) - Manage MCP servers * [Settings commands](/reference/cli/settings) - Configure pflow # pflow list Source: https://docs.pflow.run/reference/cli/list List saved workflows with keyword filtering ## Usage ```bash theme={null} pflow list [KEYWORD...] pflow list --json ``` `pflow list` shows saved workflows. Keywords use AND logic and search both workflow names and descriptions. ## Examples ```bash theme={null} pflow list pflow list github pr pflow list --json ``` # pflow mcp Source: https://docs.pflow.run/reference/cli/mcp Manage MCP server connections The `pflow mcp` command group manages Model Context Protocol (MCP) server connections. MCP servers expose external tools that can be used in pflow workflows. **Setup commands.** You run `add`, `remove`, and `sync` to configure MCP servers. Your agent uses `tools` and `info` to discover available tools when building workflows. ## Commands | Command | Description | | --------- | --------------------------- | | `add` | Add MCP servers from config | | `servers` | List configured servers | | `remove` | Remove a server | | `sync` | Discover and register tools | | `tools` | List registered MCP tools | | `info` | Show tool details | | `serve` | Run pflow as an MCP server | ## pflow mcp add Add MCP servers from config files or JSON. ```bash theme={null} pflow mcp add ... ``` **Arguments:** * `CONFIG_SOURCE` - One or more config file paths or raw JSON strings **Examples:** ```bash theme={null} # Add from config file pflow mcp add ./github.mcp.json # Add from raw JSON (simple format) pflow mcp add '{"github": {"command": "npx", "args": ["-y", "@github/mcp-server"]}}' # Add HTTP server pflow mcp add '{"slack": {"type": "http", "url": "https://mcp.example.com/slack"}}' # Add multiple servers pflow mcp add github.mcp.json slack.mcp.json ``` **Config formats:** Simple format (recommended): ```json theme={null} { "github": { "command": "npx", "args": ["-y", "@github/mcp-server"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } } ``` Full MCP format (compatible with Claude Desktop): ```json theme={null} { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@github/mcp-server"] } } } ``` HTTP server format: ```json theme={null} { "slack": { "type": "http", "url": "https://mcp.example.com/slack", "headers": { "Authorization": "Bearer ${TOKEN}" } } } ``` Environment variables in `env` and `headers` support `${VAR}` and `${VAR:-default}` syntax. They expand at runtime. ## pflow mcp list List all configured MCP servers. ```bash theme={null} pflow mcp servers [--json] ``` **Options:** * `--json` - Output as JSON **Output:** ``` Configured MCP servers: github: Transport: stdio Command: npx -y @github/mcp-server Environment: GITHUB_TOKEN=${GITHUB_TOKEN} slack: Transport: http URL: https://mcp.example.com/slack ``` ## pflow mcp remove Remove an MCP server configuration. ```bash theme={null} pflow mcp remove [--force] ``` **Arguments:** * `NAME` - Server name to remove **Options:** * `--force, -f` - Skip confirmation prompt **Examples:** ```bash theme={null} # Remove with confirmation pflow mcp remove github # Remove without confirmation pflow mcp remove github --force ``` Removing a server also removes all its tools from the registry. ## pflow mcp sync Discover and register tools from MCP servers. ```bash theme={null} pflow mcp sync [NAME] [--all] ``` **Arguments:** * `NAME` - Server name to sync (conflicts with `--all`) **Options:** * `--all, -a` - Sync all configured servers **Examples:** ```bash theme={null} # Sync specific server pflow mcp sync github # Sync all servers pflow mcp sync --all ``` **Output:** ``` Syncing server 'github'... ✓ Discovered 15 tools ✓ Registered 15 tools in pflow registry Registered tools: - mcp-github-create-issue - mcp-github-list-issues - mcp-github-get-issue ... and 12 more ``` pflow auto-syncs servers when workflows run. Manual sync is useful for immediate testing or troubleshooting. ## pflow mcp list List registered MCP tools. ```bash theme={null} pflow mcp list [SERVER] [--json] ``` **Arguments:** * `SERVER` - Filter by server name (optional) **Options:** * `--json` - Output as JSON **Examples:** ```bash theme={null} # List all MCP tools pflow mcp list # List tools from specific server pflow mcp list github # JSON output pflow mcp list --json ``` **Output:** ``` Registered MCP tools: github (15 tools): - mcp-github-create-issue: Create a new issue - mcp-github-list-issues: List repository issues - mcp-github-get-issue: Get issue details ... slack (8 tools): - mcp-slack-send-message: Send a message - mcp-slack-list-channels: List channels ... ``` ## pflow mcp describe Show detailed information about an MCP tool. ```bash theme={null} pflow mcp describe ``` **Arguments:** * `TOOL` - Tool name to inspect **Example:** ```bash theme={null} pflow mcp describe mcp-github-create-issue ``` **Output:** ``` Tool: mcp-github-create-issue Server: github Description: Create a new issue in a GitHub repository Parameters: - repo (string, required): Repository in owner/repo format - title (string, required): Issue title - body (string): Issue body/description - labels (array): List of label names Outputs: - result (object): Created issue details ``` ## pflow mcp serve Run pflow as an MCP server for AI tools. ```bash theme={null} pflow mcp serve [--debug] ``` **Options:** * `--debug` - Enable debug logging This command starts pflow as an MCP server using stdio transport. AI tools connect to pflow and can use its workflow capabilities. This is typically invoked by AI tools automatically, not run directly. See the [integrations guide](/integrations/overview) for setup instructions. **Exposed tools:** The MCP server exposes tools for: * Discovering workflows and nodes * Running workflows * Validating workflows * Saving workflows to the library * Managing settings ## Tool naming convention MCP tools are registered with the pattern `mcp-{server}-{tool}`: | Format | Example | | ----------------- | ------------------------- | | Full | `mcp-github-create-issue` | | Server-qualified | `github-create-issue` | | Short (if unique) | `create-issue` | Agents can use the shortest unambiguous name in workflows. ## Using MCP tools in workflows After syncing, MCP tools work like any other node: ```markdown theme={null} ### create-issue Create a bug report issue on GitHub. - type: mcp-github-create-issue - repo: owner/repo - title: Bug report - body: Description of the bug ``` ## File locations | Path | Purpose | | --------------------------- | --------------------- | | `~/.pflow/mcp-servers.json` | Server configurations | | `~/.pflow/registry.json` | Registered tools | ## Common workflows ### Initial setup ```bash theme={null} # Add servers pflow mcp add github.mcp.json # Sync to discover tools pflow mcp sync --all # Verify tools pflow mcp list ``` ### Troubleshooting ```bash theme={null} # Check configured servers +pflow mcp servers # Resync a server pflow mcp sync github # Get tool details pflow mcp describe mcp-github-create-issue ``` ## Related * [Adding MCP servers guide](/guides/adding-mcp-servers) - Detailed setup guide * [CLI overview](/reference/cli/index) - Main pflow command * [MCP node reference](/reference/nodes/mcp) - Using MCP tools in workflows # pflow probe Source: https://docs.pflow.run/reference/cli/probe Execute a single node and inspect its structure ## Usage ```bash theme={null} pflow probe [PARAMS...] pflow probe [PARAMS...] --output-format json ``` `pflow probe` executes a single node for exploratory testing. By default it returns metadata and template paths rather than dumping raw output. # pflow save Source: https://docs.pflow.run/reference/cli/save Save a .pflow.md workflow into the library ## Usage ```bash theme={null} pflow save --name [OPTIONS] ``` ## Examples ```bash theme={null} pflow save ./workflow.pflow.md --name my-workflow pflow save ./draft.pflow.md --name finalized-workflow --delete-draft pflow save ./workflow.pflow.md --name my-workflow --force ``` # pflow settings Source: https://docs.pflow.run/reference/cli/settings Manage pflow configuration The `pflow settings` command group manages pflow configuration stored in `~/.pflow/settings.json`. This includes API keys, environment variables, and node filtering. **Your commands.** These are setup commands that you run directly - not your agent. API keys and security settings should always be configured by you, never by an AI agent. ## Commands | Command | Description | | ---------------------- | ------------------------------------------------ | | `init` | Initialize settings with defaults | | `show` | Display current settings | | `set-env` | Set an environment variable | | `unset-env` | Remove an environment variable | | `list-env` | List all environment variables | | `allow` | Add an allow pattern | | `deny` | Add a deny pattern | | `remove` | Remove a pattern | | `check` | Check if a node is allowed | | `reset` | Reset to defaults | | `llm show` | Show LLM model settings | | `llm set-default` | Set default model for all features | | `llm set-discovery` | Set model for discovery commands | | `llm set-filtering` | Set model for smart filtering | | `llm set-tts-model` | Set the TTS model for `pflow ui --say` narration | | `llm set-tts-voice` | Set the TTS voice for `pflow ui --say` narration | | `llm unset` | Remove an LLM model setting | | `registry output-mode` | Set registry run output display mode | ## pflow settings init Initialize settings file with defaults. ```bash theme={null} pflow settings init ``` Creates `~/.pflow/settings.json` with default configuration. Prompts for confirmation if the file already exists. **Default settings:** ```json theme={null} { "version": "1.0.0", "registry": { "nodes": { "allow": ["*"], "deny": [] }, "output_mode": "smart" }, "llm": { "default_model": null, "discovery_model": null, "filtering_model": null }, "env": {} } ``` ## pflow settings show Display current settings with sensitive values masked. ```bash theme={null} pflow settings show ``` **Output:** ``` Settings file: ~/.pflow/settings.json Current settings: { "version": "1.0.0", "registry": { "nodes": { "allow": ["*"], "deny": [] } }, "llm": { "default_model": "openai/gpt-5.2", "discovery_model": null, "filtering_model": null }, "env": { "ANTHROPIC_API_KEY": "sk-***", "log_level": "debug" } } ``` Sensitive values (API keys, tokens, secrets) are automatically masked. Use `pflow settings list-env --show-values` to see full values. ## pflow settings set-env Set an environment variable for pflow workflows. ```bash theme={null} pflow settings set-env ``` **Arguments:** * `KEY` - Environment variable name * `VALUE` - Environment variable value **Examples:** ```bash theme={null} # Set API keys for LLM providers pflow settings set-env OPENAI_API_KEY "sk-..." pflow settings set-env ANTHROPIC_API_KEY "sk-ant-..." # Set other variables pflow settings set-env GITHUB_TOKEN "ghp_..." ``` **Output:** ``` ✓ Set environment variable: OPENAI_API_KEY Value: sk-*** ``` **Security:** Always set API keys yourself - never let AI agents run this command. **Alternative:** Set the key as a shell environment variable instead — pflow (via LiteLLM) reads `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, and provider-specific equivalents directly from `os.environ`. ## pflow settings unset-env Remove an environment variable. ```bash theme={null} pflow settings unset-env ``` **Arguments:** * `KEY` - Environment variable name to remove **Example:** ```bash theme={null} pflow settings unset-env GITHUB_TOKEN ``` ## pflow settings list-env List all configured environment variables. ```bash theme={null} pflow settings list-env [--show-values] ``` **Options:** * `--show-values` - Display full unmasked values (use with caution) **Examples:** ```bash theme={null} # List with masked values (safe) pflow settings list-env # List with full values (sensitive!) pflow settings list-env --show-values ``` **Output (masked):** ``` Environment variables: OPENAI_API_KEY: sk-*** GITHUB_TOKEN: ghp*** LOG_LEVEL: debug ``` Only use `--show-values` in secure environments. Never let agents access unmasked credentials. ## pflow settings allow Add an allow pattern for node filtering. ```bash theme={null} pflow settings allow ``` **Arguments:** * `PATTERN` - Glob-style pattern for nodes to allow **Examples:** ```bash theme={null} # Allow all file nodes pflow settings allow "pflow.nodes.file.*" # Allow specific MCP tools pflow settings allow "mcp-github-*" # Allow specific node pflow settings allow "llm" ``` **Pattern syntax:** * `*` matches any characters * `?` matches single character * `[seq]` matches any character in seq ## pflow settings deny Add a deny pattern for node filtering. ```bash theme={null} pflow settings deny ``` **Arguments:** * `PATTERN` - Glob-style pattern for nodes to deny **Examples:** ```bash theme={null} # Block dangerous operations pflow settings deny "shell" pflow settings deny "*-delete-*" ``` Deny patterns take precedence over allow patterns. ## pflow settings remove Remove a pattern from allow or deny list. ```bash theme={null} pflow settings remove [--allow|--deny] ``` **Arguments:** * `PATTERN` - Pattern to remove **Options:** * `--allow` - Remove from allow list (default) * `--deny` - Remove from deny list **Examples:** ```bash theme={null} # Remove from deny list pflow settings remove "shell" --deny # Remove from allow list pflow settings remove "file.*" --allow ``` ## pflow settings check Check if a node would be included based on current settings. ```bash theme={null} pflow settings check ``` **Arguments:** * `NODE_NAME` - Node name to check **Example:** ```bash theme={null} pflow settings check read-file ``` **Output (included):** ``` ✓ Node 'read-file' would be INCLUDED Matched allow patterns: file.*, * ``` **Output (excluded):** ``` ✗ Node 'shell' would be EXCLUDED Matched deny patterns: shell ``` ## pflow settings reset Reset settings to defaults. ```bash theme={null} pflow settings reset ``` Prompts for confirmation before resetting. This removes all custom settings including API keys. This deletes ALL custom settings including environment variables and API keys. ## LLM model settings These commands let you override which models pflow uses for its internal features. By default, pflow auto-detects based on your configured API keys - these are optional. ### pflow settings llm show Display LLM model settings with resolution status. ```bash theme={null} pflow settings llm show ``` **Output:** ``` LLM Model Settings: default_model: openai/gpt-5.2 (configured) discovery_model: (using default_model → openai/gpt-5.2) filtering_model: (using default_model → openai/gpt-5.2) tts_model: gemini-3.1-flash-tts-preview tts_voice: Kore Resolution order: default: workflow params → default_model → auto-detect → error discovery: discovery_model → default_model → auto-detect → fallback filtering: filtering_model → default_model → auto-detect → fallback To configure: pflow settings llm set-default pflow settings llm set-discovery pflow settings llm set-filtering pflow settings llm set-tts-model pflow settings llm set-tts-voice ``` ### pflow settings llm set-default Override the auto-detected model for all pflow LLM usage. ```bash theme={null} pflow settings llm set-default ``` **Arguments:** * `MODEL` - Model identifier (e.g., `openai/gpt-5.2`, `anthropic/claude-sonnet-4-5`, `gemini/gemini-3-flash-preview`). LiteLLM requires the provider prefix. When set, this model is used instead of auto-detection for: * LLM nodes in workflows (when no model specified in params) * Discovery commands (when `discovery_model` not set) * Smart filtering (when `filtering_model` not set) **Examples:** ```bash theme={null} # Set OpenAI model pflow settings llm set-default openai/gpt-5.2 # Set Anthropic model pflow settings llm set-default anthropic/claude-sonnet-4-5 # Set Google model pflow settings llm set-default gemini/gemini-3-flash-preview ``` This is optional - pflow auto-detects a model based on your API keys. Use this if you want a specific model instead of the auto-detected one. **Bare model names are auto-prefixed.** Bare names from known providers (`gpt-*`, `o1*`, `o3*`, `o4*`, `claude-*`, `gemini-*`) are rewritten to the canonical `/` form at write time, with a `Normalized:` line confirming the rewrite. Unknown bare names pass through with a warning so custom or self-hosted models still work. This applies to `set-default`, `set-discovery`, and `set-filtering` — not to the TTS setters (TTS ids aren't LiteLLM-routed). ```bash theme={null} $ pflow settings llm set-default gemini-3-flash-preview Normalized: gemini/gemini-3-flash-preview ✓ Set default_model: gemini/gemini-3-flash-preview ``` ### pflow settings llm set-discovery Set the model for discovery commands (`pflow mcp find`, `pflow find`). ```bash theme={null} pflow settings llm set-discovery ``` **Arguments:** * `MODEL` - Model identifier **Example:** ```bash theme={null} # Use a fast, cheap model for discovery pflow settings llm set-discovery gemini/gemini-3-flash-preview ``` ### pflow settings llm set-filtering Set the model for smart field filtering (used when `smart` output mode filters large API responses). ```bash theme={null} pflow settings llm set-filtering ``` **Arguments:** * `MODEL` - Model identifier **Example:** ```bash theme={null} # Use a fast, cheap model for filtering pflow settings llm set-filtering gemini/gemini-2.5-flash-lite ``` Smart filtering is a simple task - use a fast, cheap model. **Recommended (fast + cheap):** | Provider | Model | Overhead | Notes | | --------- | ------------------------------ | -------- | ------------------ | | Google | `gemini/gemini-2.5-flash-lite` | \~2-3s | Best budget option | | OpenAI | `openai/gpt-5-mini` | \~1-2s | Runner-up budget | | Anthropic | `anthropic/claude-haiku-4-5` | \~1-2s | Third place budget | **Alternative (higher cost):** | Provider | Model | Overhead | Notes | | --------- | ------------------------------ | -------- | --------------------- | | Anthropic | `anthropic/claude-sonnet-5` | \~1-2s | Best premium option | | Google | `gemini/gemini-3.5-flash-lite` | \~2-3s | Cheap, fast runner-up | | OpenAI | `openai/gpt-5.6-luna` | varies | Common fallback | All models produce equivalent quality for this task. Timings are approximate and vary with network latency. ### pflow settings llm set-tts-model / set-tts-voice Configure voice narration for `pflow ui focus/frame --say` (see the [UI command](/reference/cli#ui-command)). Both have working defaults — you only need these to change the voice or track a new TTS model. Narration also needs a Gemini API key (`pflow settings set-env GEMINI_API_KEY `). ```bash theme={null} pflow settings llm set-tts-model # a Gemini TTS model id pflow settings llm set-tts-voice # a Gemini prebuilt voice name, e.g. Kore (default), Puck ``` **Example:** ```bash theme={null} pflow settings llm set-tts-voice Puck ``` ### pflow settings llm unset Remove an LLM model setting. Models revert to auto-detection; the TTS fields revert to their built-in defaults. ```bash theme={null} pflow settings llm unset {default|discovery|filtering|tts-model|tts-voice|all} ``` **Arguments:** * `SETTING` - Which setting to remove: `default`, `discovery`, `filtering`, `tts-model`, `tts-voice`, or `all` **Examples:** ```bash theme={null} # Clear default model pflow settings llm unset default # Clear discovery model (will use default_model or auto-detect) pflow settings llm unset discovery # Back to the default narration voice pflow settings llm unset tts-voice # Clear all LLM settings pflow settings llm unset all ``` ### Model resolution order pflow uses the same resolution order for all LLM usage (discovery, filtering, and workflow LLM nodes): 1. Explicit setting (workflow params or feature-specific setting) 2. `default_model` from settings 3. Auto-detect from configured API keys (Anthropic → Google → OpenAI) 4. Error with setup instructions **Auto-detected defaults by provider:** | Provider | Default model | | --------- | ------------------------------ | | Anthropic | `anthropic/claude-sonnet-5` | | Google | `gemini/gemini-3.5-flash-lite` | | OpenAI | `openai/gpt-5.6-luna` | Most users just need an API key configured. pflow auto-detects the appropriate model. Use these commands only to override the auto-detected model. ## Registry settings Configure how `pflow probe` displays output. ### pflow settings registry output-mode Show or set the output display mode for `pflow probe`. ```bash theme={null} pflow settings registry output-mode [MODE] ``` **Arguments:** * `MODE` (optional) - One of `smart`, `structure`, or `full`. If omitted, shows current mode. **Modes:** | Mode | Description | | ----------------- | --------------------------------------------------------------------------------------------------- | | `smart` (default) | Shows template paths with values. Uses LLM to filter large outputs (>30 fields) to relevant fields. | | `structure` | Shows template paths only (no values). No filtering - shows all fields. Fast, no LLM overhead. | | `full` | Shows all fields with full values, no truncation or filtering. | **Examples:** ```bash theme={null} # Show current mode pflow settings registry output-mode # Set to smart (default - shows values with truncation) pflow settings registry output-mode smart # Set to structure-only (paths without values) pflow settings registry output-mode structure # Set to full (all values, no truncation) pflow settings registry output-mode full ``` Use `structure` mode when working with sensitive data — it shows types and paths without actual values, so your agent can build workflows without ever seeing the data itself. Avoid `full` mode with AI agents — it shows all values without truncation and can consume excessive tokens. See [pflow probe](/reference/cli/probe) for the command that uses these output modes. ## How node filtering works Node filtering uses allow and deny patterns evaluated in this order: 1. **Deny patterns** - Block matching nodes (highest precedence) 2. **Allow patterns** - Include matching nodes 3. **Default** - Include if `*` in allow list **Example configuration:** ```json theme={null} { "registry": { "nodes": { "allow": ["*"], "deny": ["shell"] } } } ``` This allows all nodes except test nodes and the shell node. ## Environment variable precedence When workflows need parameters, pflow looks in this order: 1. CLI parameters (`key=value` arguments) 2. Settings environment variables (`pflow settings set-env`) 3. Workflow defaults 4. Error if required and not found **Example:** ```bash theme={null} # Store API key once pflow settings set-env OPENAI_API_KEY "sk-..." # Workflows automatically use it pflow my-llm-workflow # No need to pass --param ``` ## Sensitive parameter detection These keys are automatically masked in output: * `password`, `passwd`, `pwd` * `token`, `api_token`, `access_token`, `auth_token` * `api_key`, `apikey`, `api-key` * `secret`, `client_secret`, `secret_key` * `private_key`, `ssh_key` Matching is case-insensitive. ## File locations | Path | Purpose | | ------------------------ | ------------- | | `~/.pflow/settings.json` | Settings file | ## Related * [CLI overview](/reference/cli/index) - Main pflow command * [Configuration reference](/reference/configuration) - All configuration options * [Quickstart](/quickstart) - Initial setup including API keys # pflow skill Source: https://docs.pflow.run/reference/cli/skill Publish workflows as AI agent skills The `pflow skill` command group publishes saved workflows as [Agent Skills](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills) that AI coding tools can discover and use. pflow creates symlinks from tool skill directories to your saved workflows, keeping `~/.pflow/workflows/` as the single source of truth. ## Supported tools | Tool | Project directory | Personal directory | | --------------------- | ----------------- | -------------------- | | Claude Code (default) | `.claude/skills/` | `~/.claude/skills/` | | Cursor | `.cursor/skills/` | `~/.cursor/skills/` | | Codex | `.agents/skills/` | `~/.agents/skills/` | | Copilot | `.github/skills/` | `~/.copilot/skills/` | ## Commands | Command | Description | | -------- | ----------------------------- | | `save` | Publish a workflow as a skill | | `list` | List all pflow-managed skills | | `remove` | Remove a skill | ## pflow skill save Publish a saved workflow as a skill for AI coding tools. ```bash theme={null} pflow skill save [OPTIONS] ``` **Arguments:** * `WORKFLOW_NAME` - Name of a saved workflow (required) **Options:** | Option | Description | | ------------ | --------------------------------------------- | | `--personal` | Save to personal directory instead of project | | `--cursor` | Save to Cursor | | `--codex` | Save to Codex | | `--copilot` | Save to Copilot | By default, saves to Claude Code. Use multiple flags to save to multiple tools at once. **Examples:** ```bash theme={null} # Publish to Claude Code (default) pflow skill save pr-analyzer # Publish to Cursor pflow skill save pr-analyzer --cursor # Publish to multiple tools pflow skill save pr-analyzer --cursor --copilot # Publish to personal directory (cross-project) pflow skill save pr-analyzer --personal ``` **Output:** ``` Published 'pr-analyzer' to Claude Code (.claude/skills/) Symlink: /path/to/project/.claude/skills/pr-analyzer/SKILL.md Source: /Users/you/.pflow/workflows/pr-analyzer.pflow.md ``` Running `save` again on an existing skill updates the enrichment (idempotent): ``` Updated 'pr-analyzer' in Claude Code (.claude/skills/) Symlink: /path/to/project/.claude/skills/pr-analyzer/SKILL.md Source: /Users/you/.pflow/workflows/pr-analyzer.pflow.md ``` When you run `pflow skill save`, two things happen: 1. **Workflow enrichment** - pflow adds a `## Usage` section to your workflow file with instructions for AI agents, and adds `name` and `description` fields to the frontmatter. 2. **Symlink creation** - pflow creates a `SKILL.md` symlink in the tool's skill directory pointing to your workflow file. The enrichment is idempotent - running save multiple times just updates the `## Usage` section. ## pflow skill list List all pflow-managed skills across all tools. ```bash theme={null} pflow skill list ``` **Output:** ``` pflow skills: pr-analyzer → Claude Code (project) → Cursor (project) data-processor → Claude Code (personal) ``` If a skill's source workflow was deleted, the list shows it as a broken link with fix instructions: ``` pflow skills: old-workflow → Claude Code (project) [broken link] Broken link: the source workflow 'old-workflow' was deleted. To restore: pflow save --name old-workflow --force To remove: pflow skill remove old-workflow ``` ## pflow skill remove Remove a workflow's skill from tool directories. ```bash theme={null} pflow skill remove [OPTIONS] ``` **Arguments:** * `WORKFLOW_NAME` - Name of the skill to remove (required) **Options:** | Option | Description | | ------------ | ------------------------------------------------- | | `--personal` | Remove from personal directory instead of project | | `--cursor` | Remove from Cursor | | `--codex` | Remove from Codex | | `--copilot` | Remove from Copilot | By default, removes from Claude Code. Use multiple flags to remove from multiple tools. **Examples:** ```bash theme={null} # Remove from Claude Code (default) pflow skill remove pr-analyzer # Remove from Cursor pflow skill remove pr-analyzer --cursor # Remove from multiple tools pflow skill remove pr-analyzer --cursor --copilot # Remove from personal directory pflow skill remove pr-analyzer --personal ``` **Output:** ``` Removed skill 'pr-analyzer' from Claude Code (.claude/skills/) ``` Removing a skill only deletes the symlink. The saved workflow in `~/.pflow/workflows/` is unchanged, including any enrichment added during `skill save`. ## Workflow requirements Before publishing a workflow as a skill, you must save it to the global library: ```bash theme={null} # First, save the workflow pflow save ./my-workflow.pflow.md --name my-workflow # Then publish as a skill pflow skill save my-workflow ``` ## Project vs personal skills **Project skills** (default) are stored in the current project directory and are typically version-controlled with your project. Use these for project-specific workflows. **Personal skills** (`--personal`) are stored in your home directory and are available across all projects. Use these for general-purpose workflows you want everywhere. ## Related * [Workflow commands](/reference/cli/list) - Save and manage workflows * [Claude Code integration](/integrations/claude-code) - Set up pflow with Claude Code * [Cursor integration](/integrations/cursor) - Set up pflow with Cursor # Configuration Source: https://docs.pflow.run/reference/configuration Settings file, environment variables, and file locations pflow stores configuration in `~/.pflow/`. This page covers the settings file structure, environment variables, node filtering, and file locations. ## Settings file pflow stores user configuration in `~/.pflow/settings.json`. View current settings with: ```bash theme={null} pflow settings show ``` ### Structure ```json theme={null} { "version": "1.0.0", "registry": { "nodes": { "allow": ["*"], "deny": [] }, "output_mode": "smart" }, "runtime": { "template_resolution_mode": "strict" }, "llm": { "default_model": null, "discovery_model": null, "filtering_model": null }, "env": {} } ``` ### Fields | Field | Default | Description | | ---------------------------------- | -------------------------------- | ----------------------------------------------------------------------------- | | `registry.nodes.allow` | `["*"]` | Patterns for nodes to include | | `registry.nodes.deny` | `[]` | Patterns for nodes to exclude | | `registry.output_mode` | `"smart"` | Output display mode for `registry run`: `"smart"`, `"structure"`, or `"full"` | | `runtime.template_resolution_mode` | `"strict"` | `"strict"` or `"permissive"` | | `llm.default_model` | `null` | Default model for all pflow LLM usage | | `llm.discovery_model` | `null` | Model for discovery commands (overrides default) | | `llm.filtering_model` | `null` | Model for smart filtering (overrides default) | | `llm.tts_model` | `"gemini-3.1-flash-tts-preview"` | TTS model for `pflow ui --say` narration | | `llm.tts_voice` | `"Kore"` | TTS voice for `pflow ui --say` narration | | `env` | `{}` | API keys and environment variables | ### Commands ```bash theme={null} # Initialize settings file pflow settings init # Show current settings pflow settings show # Reset to defaults pflow settings reset ``` ## LLM model configuration pflow uses LLMs for discovery features, smart filtering, and the LLM node in workflows. Set up an API key, and pflow auto-detects which model to use. ### API key setup ```bash theme={null} # Set an API key (any llm-supported provider works) pflow settings set-env OPENAI_API_KEY "sk-..." ``` pflow auto-detects available providers based on configured API keys. ### Override the model (optional) To use a specific model instead of auto-detection: ```bash theme={null} # Set a default model for all pflow features pflow settings llm set-default openai/gpt-5.2 # Or configure specific features pflow settings llm set-discovery anthropic/claude-haiku-4-5 pflow settings llm set-filtering gemini/gemini-3-flash-preview # View current configuration pflow settings llm show ``` ### Resolution order pflow auto-detects a model based on your configured API keys. To override this or see the full resolution order and default models, see [LLM model settings](/reference/cli/settings#llm-model-settings). ## Environment variables ### API keys Store API keys in the settings file for security (automatically chmod 600): ```bash theme={null} pflow settings set-env ANTHROPIC_API_KEY "sk-ant-..." pflow settings set-env OPENAI_API_KEY "sk-proj-..." ``` API keys stored via `pflow settings set-env` are used automatically for workflow inputs and discovery features. Your agent can't set these for security reasons. **Precedence order** (highest to lowest): 1. CLI parameters (`--param key=value`) 2. Settings file (`~/.pflow/settings.json` → `env`) 3. System environment variables ### pflow configuration variables | Variable | Default | Description | | -------------------------------- | -------- | ------------------------------ | | `PFLOW_TEMPLATE_RESOLUTION_MODE` | `strict` | `strict` or `permissive` | | `PFLOW_SHELL_STRICT` | `false` | Block dangerous shell commands | ### Trace configuration Control trace file verbosity: | Variable | Default | Description | | --------------------------- | ------- | ----------------------------- | | `PFLOW_TRACE_PROMPT_MAX` | `50000` | Max prompt length in traces | | `PFLOW_TRACE_RESPONSE_MAX` | `20000` | Max response length in traces | | `PFLOW_TRACE_STORE_MAX` | `10000` | Max shared store value length | | `PFLOW_TRACE_DICT_MAX` | `50000` | Max dict size in traces | | `PFLOW_TRACE_LLM_CALLS_MAX` | `100` | Max LLM calls to track | ## Node filtering Control which nodes are available using allow/deny patterns. ### Pattern syntax pflow uses glob-style patterns (fnmatch): | Pattern | Matches | | -------------------- | -------------------- | | `*` | Everything | | `pflow.nodes.file.*` | All file nodes | | `mcp-github-*` | All GitHub MCP tools | | `shell` | Exact match only | ### Evaluation order 1. **Deny patterns** - If matched, node is excluded 2. **Allow patterns** - If matched, node is included 3. **Default** - Included only if `*` is in allow list Deny patterns take precedence over allow patterns. If a node matches both, it's excluded. ### Commands ```bash theme={null} # Allow a pattern pflow settings allow "pflow.nodes.file.*" # Deny a pattern pflow settings deny "shell" # Remove a pattern pflow settings remove "shell" --deny # Check if a node would be included pflow settings check read-file # Verify filtering took effect pflow mcp list # See all available nodes pflow mcp list file # Filter to specific nodes ``` ### Examples **Block shell access**: ```bash theme={null} pflow settings deny "shell" ``` **Allow only specific nodes**: ```bash theme={null} # First, remove the wildcard pflow settings remove "*" --allow # Then add specific patterns pflow settings allow "pflow.nodes.file.*" pflow settings allow "llm" pflow settings allow "http" ``` ## File locations pflow stores all data in `~/.pflow/`: ``` ~/.pflow/ ├── settings.json # User settings + API keys ├── mcp-servers.json # MCP server configurations ├── registry.json # Node registry cache ├── workflows/ # Saved workflows ├── debug/ # Trace files └── cache/ # Execution cache ``` ### What's safe to delete | Path | Safe to Delete | Notes | | ------------------ | -------------- | ----------------------------------------------- | | `registry.json` | Yes | Regenerates automatically on next pflow startup | | `debug/` | Yes | Debug traces only, can grow large | | `cache/` | Yes | Execution cache, regenerates | | `settings.json` | Caution | Contains API keys, reverts to defaults | | `mcp-servers.json` | Caution | Removes MCP server configurations | | `workflows/` | No | User-created workflows | ## MCP server configuration MCP servers are configured in `~/.pflow/mcp-servers.json`. See [Adding MCP servers](/guides/adding-mcp-servers) for details. The file uses the standard MCP configuration format: ```json theme={null} { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@github/mcp-server"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } } } } ``` Environment variables in the config (`${VAR}`) are resolved from: 1. System environment variables 2. pflow settings (`pflow settings set-env`) # Experimental features Source: https://docs.pflow.run/reference/experimental Features that are available but not yet stable These features are available but experimental — they may change, have rough edges, or be removed in future versions. Use them if they solve a problem, but know the caveats. ## Feedback If you use these experimental features and have feedback: * **Questions or ideas** - Start a [discussion](https://github.com/spinje/pflow/discussions) * **Bug reports** - Open an [issue](https://github.com/spinje/pflow/issues) # Agent Source: https://docs.pflow.run/reference/nodes/agent Run Claude Code or OpenAI Codex for repository-aware tasks **Agent commands.** Your AI agent uses this node in workflows. You don't configure it directly. The `agent` node gives a workflow an autonomous coding agent that can inspect files, edit a repository, run commands, and iterate. Each node declares `backend: claude` or `backend: codex`; the workflow-facing result and structured-output behavior stay the same across both. Use `llm` for text processing that does not need repository tools. Use `code`, `shell`, or `http` when one deterministic operation is enough. ## Parameters ### Shared parameters | Parameter | Type | Required | Default | Description | | ---------------- | ---- | -------- | ----------------- | -------------------------------------------------------------------------------------------- | | `backend` | str | Yes | - | `claude` or `codex` | | `prompt` | str | Yes | - | Task sent to the agent | | `inputs` | dict | No | - | Named values available to a file-backed prompt template | | `output_schema` | dict | No | - | Top-level object JSON Schema for structured output | | `cwd` | str | No | Current directory | Working directory | | `model` | str | No | Backend default | Model override. Claude defaults to `claude-sonnet-4-5`; Codex inherits its CLI configuration | | `timeout` | int | No | `300` | Execution timeout in seconds (30–3600) | | `system_prompt` | str | No | - | Additional backend system/developer instructions | | `resume` | str | No | - | Session/thread ID from a previous agent call | | `schema_retries` | int | No | `1` | Corrective structured-output attempts (0–5) | | `use_api_key` | bool | No | `false` | Permit API-key or configured-provider billing | Parameters remain flat. A backend-only parameter used with the other backend is a validation error, so a workflow cannot silently ignore a misspelled or misplaced permission setting. ### Claude parameters | Parameter | Type | Required | Default | Description | | --------------------- | ---- | -------- | --------- | ------------------------------------------------------ | | `allowed_tools` | list | No | All tools | Permitted Claude tools or patterns | | `disallowed_tools` | list | No | None | Denied Claude tools or patterns | | `max_turns` | int | No | `50` | Maximum turns (1–100; at least 2 with `output_schema`) | | `max_thinking_tokens` | int | No | `8000` | Reasoning budget (1000–100000) | | `sandbox` | dict | No | None | Claude Agent SDK sandbox settings | Claude sandbox keys include `enabled`, `autoAllowBashIfSandboxed`, `excludedCommands`, `allowUnsandboxedCommands`, `network`, `enableWeakerNestedSandbox`, and `ignoreViolations`. The backend passes unknown keys through for SDK forward compatibility while validating known value shapes. ### Codex parameters | Parameter | Type | Required | Default | Description | | ----------------- | ---- | -------- | ----------------- | -------------------------------------------------- | | `sandbox` | str | No | `workspace-write` | `read-only`, `workspace-write`, or `full-access` | | `approval_policy` | str | No | CLI configuration | `untrusted`, `on-request`, or `never` | | `add_dir` | list | No | `[]` | Additional writable directories for an initial run | | `profile` | str | No | - | Codex CLI configuration profile | | `config` | dict | No | `{}` | TOML-compatible Codex configuration overrides | `full-access` maps to the Codex CLI's `danger-full-access` mode. Dedicated node parameters take precedence over the same key in `config`. ## Output | Key | Type | Description | | --------------- | -------- | --------------------------------------------------------------------------------- | | `result` | str/dict | Free-form text, parsed structured output, or raw text after a schema soft-failure | | `llm_usage` | dict | Normalized token, duration, turn, model, and session metadata | | `_schema_error` | str | Present when structured output could not be validated or recovered | Schema soft-failures make the workflow `DEGRADED`, but the node still follows its normal success route. They do not trigger `on-error`; downstream logic should branch on the result shape or handle `_schema_error` in a fallback path. ### Usage metadata Both backends write these fields when usage is available: ```json theme={null} { "model": "backend-model-or-null", "input_tokens": 14653, "uncached_input_tokens": 4669, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 9984, "input_token_accounting": "total_includes_cache", "output_tokens": 25, "total_tokens": 14678, "cost_usd": null, "api_equivalent_cost_usd": null, "duration_ms": 3450, "num_turns": 1, "session_id": "019f..." } ``` Claude reports `input_token_accounting: split_cache_fields` and exposes the SDK comparison as `api_equivalent_cost_usd`. Codex reports `total_includes_cache` and adds `reasoning_output_tokens`. When a workflow declares `model` and the CLI emitted usage, pflow estimates `api_equivalent_cost_usd` from its LiteLLM pricing catalog and cache-aware token usage. The estimate remains `null` when usage is missing, `model` is omitted, or LiteLLM has no pricing; pflow does not inspect private Codex session files to infer the CLI-configured model. Agent backends leave canonical `cost_usd` as `null` because neither transport proves what the provider actually billed. API-equivalent cost is an estimate for comparison and planning, not evidence that a subscription-backed call incurred per-token charges. When schema recovery makes corrective calls, the top-level fields describe the final call and `retries` contains usage records for superseded calls. Reports and trace summaries aggregate the final call and those retry records. ## Choose a backend ```markdown theme={null} ### implement Fix the failing test and report what changed. - type: agent - backend: claude - max_turns: 6 - allowed_tools: [Read, Edit, Bash] - prompt: Fix the failing test, keep the patch focused, and run the smallest relevant test command. ``` Claude-specific tool lists and sandbox settings let the workflow constrain the Claude Agent SDK surface. ```markdown theme={null} ### implement Fix the failing test and report what changed. - type: agent - backend: codex - sandbox: workspace-write - approval_policy: never - prompt: Fix the failing test, keep the patch focused, and run the smallest relevant test command. ``` Codex runs through `codex exec` with shell-free arguments and isolated stdin. `workspace-write` is the normal editing mode; use `read-only` for analysis and reserve `full-access` for tasks that must write outside the workspace sandbox. ## Structured output Both backends use their native JSON Schema support. The schema must declare top-level `type: object`; wrap arrays, primitives, or top-level combinators inside an object property. ````markdown theme={null} ### review Review the current change and return structured findings. - type: agent - backend: codex - sandbox: read-only ```yaml output_schema type: object properties: summary: { type: string } issues: type: array items: { type: string } approved: { type: boolean } required: [summary, issues, approved] ``` ```prompt Review the current change for correctness and missing tests. Do not edit files. ``` ```` The parsed object is available at `${review.result}` and fields such as `${review.result.approved}`. Pflow canonically coerces type-wrong scalar values, then asks the same session for a corrected object when needed. `schema_retries` controls those continuation calls. If the backend produces no session ID or still misses the schema, pflow preserves raw text in `result`, sets `_schema_error`, and marks the workflow `DEGRADED`. Set `schema_retries: 0` when you want native structured output without pflow's conformance check or corrective continuation. ## Resume a session Both backends expose a persistent identifier as `llm_usage.session_id`: ```markdown theme={null} ### investigate - type: agent - backend: claude - prompt: Diagnose the failure and propose a fix. Do not edit yet. ### implement - type: agent - backend: claude - resume: ${investigate.llm_usage.session_id} - prompt: Implement the proposed fix and run the focused tests. ``` Claude resumes by SDK session ID. Codex resumes its on-disk CLI thread, so the ID also works across separate pflow invocations. A resumed Codex command does not re-apply initial `cwd` or `add_dir` flags; keep extra-directory assumptions explicit in the task. ## Authentication `use_api_key` is shared by both backends and fails closed on ambiguous values. Pflow never logs in, stores credentials, or requires a key; `true` is explicit permission for API-key or configured-provider billing. ```bash theme={null} npm install -g @anthropic-ai/claude-code claude auth login claude auth status ``` With `use_api_key: false`, the node blanks `ANTHROPIC_API_KEY` for the Claude subprocess only so Claude Code can use its account/subscription login. Sibling `llm` nodes still see the original environment. Use `claude setup-token` for non-interactive subscription setup. Set `use_api_key: true` and provide `ANTHROPIC_API_KEY`: ```bash theme={null} pflow settings set-env ANTHROPIC_API_KEY "sk-ant-..." ``` This permits Anthropic Console per-token billing. The flag fails closed on ambiguous values so a string such as `"false"` cannot accidentally enable billing. ```bash theme={null} npm install -g @openai/codex codex login codex login status ``` With `use_api_key: false`, the node removes `OPENAI_API_KEY` and `CODEX_API_KEY` from a child-only environment, requires recognized ChatGPT/account access-token auth through `codex login status`, and pins the OpenAI provider selector before each model turn. Parent and sibling environments are unchanged. Set `use_api_key: true` only when you intend API-key or configured-provider billing: ```markdown theme={null} - type: agent - backend: codex - use_api_key: true - prompt: Implement the requested change and run focused tests. ``` True mode skips the account-auth preflight and preserves key, profile, and provider configuration. Existing Codex workflows that intentionally use API-key auth must add this flag or they now fail before the model call. False mode controls the named first-party key variables, recognized stored API-key auth, and ordinary provider selection. It cannot prove custom profiles, providers, proxies, or base URLs are unmetered, and it cannot prevent account credits, auto-reload, overage, or administrator policy from applying. ## Error handling | Error | Cause | Fix | | ----------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Claude CLI not found | Claude Code is not installed | `npm install -g @anthropic-ai/claude-code` | | Claude authentication failed | No subscription login, or invalid Console key in API-key mode | Run `claude auth login`, or check `ANTHROPIC_API_KEY` when `use_api_key: true` | | Codex CLI not found | Codex is not installed | `npm install -g @openai/codex` | | Codex authentication failed | Account login is missing, expired, unrecognized, or API-key auth was not opted into | Run `codex login` / `codex login status`, or add `use_api_key: true` only when API/provider billing is intended | | Timeout | The task exceeded the configured subprocess/session limit | Increase `timeout` or split the task | | Cross-backend parameter error | A Claude-only parameter was used with Codex, or vice versa | Move to the matching backend or remove the parameter | The node itself retries execution failures twice before surfacing the backend-specific error. Deterministic Codex installation and authentication failures are marked non-retriable for outer batch retries. ## Related * [LLM node](/reference/nodes/llm) — text processing without repository tools * [Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/python) # Code Source: https://docs.pflow.run/reference/nodes/code Execute Python for data transformation **Agent commands.** Your AI agent uses this node in workflows. You don't configure it directly. The code node runs Python in-process with direct access to input data as native objects. Use it when the logic is deterministic — filtering, reshaping, merging, computing values. If you can write a Python expression for it, there's no reason to burn tokens on an LLM call. ## Parameters | Parameter | Type | Required | Default | Description | | ---------- | ---- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | | `code` | str | Yes | - | Python code (or path to `.py` file, e.g., `./scripts/transform.py`) with type-annotated inputs and a `result` variable | | `inputs` | dict | No | `{}` | Variable name to value mapping (template variables go here) | | `timeout` | int | No | `30` | Maximum execution time in seconds | | `requires` | list | No | `[]` | Package dependencies (documentation-only, not enforced) | ## Output | Key | Type | Description | | -------- | ---- | ---------------------------------------------- | | `result` | any | Value of the `result` variable after execution | | `stdout` | str | Captured `print()` output | | `stderr` | str | Captured stderr output | | `error` | str | Error message (only on failure) | ## Type annotations Every input variable and the `result` variable must have a type annotation. pflow uses these to validate inputs before your code runs — so a wrong type gets caught immediately with a fix suggestion, not as a cryptic Python error mid-workflow. ```python theme={null} data: list # required for every input limit: int # required for every input result: dict = {} # required on result too ``` Use `Any` when you don't know or care what the input type is. pflow auto-injects `Any`, so you don't need `from typing import Any`. | Annotation | Python type | Notes | | ---------- | -------------- | ---------------------------------------------------------- | | `int` | `int` | | | `float` | `int`, `float` | Integers are accepted where float is declared | | `str` | `str` | | | `bool` | `bool` | | | `list` | `list` | Generic params like `list[dict]` check outer type only | | `dict` | `dict` | Generic params like `dict[str, Any]` check outer type only | | `set` | `set` | | | `tuple` | `tuple` | | | `bytes` | `bytes` | | | `Any` | — | Auto-injected. Skips type validation entirely | Only the outer type is checked — `list[dict]` validates that the value is a `list`, but doesn't check whether the elements are dicts. ## Template placement **Templates go in `inputs`, never in the code block.** The code block is literal Python — `${var}` isn't valid Python syntax, so putting templates there causes a parse error. ````markdown theme={null} ### transform Filter active users from the API response. - type: code - inputs: data: ${fetch.response} ```python code data: list result: list = [u for u in data if u['active']] ``` ```` Templates in the `inputs` dict get resolved before your code runs. By the time Python sees `data`, it's already a native object — no parsing needed. ## Bridge to workflow inputs Workflow `## Inputs` / `## Outputs` use canonical names. Code blocks use Python annotations. | Workflow `type:` | Code annotation | | ---------------- | ------------------------- | | `string` | `str` | | `integer` | `int` | | `number` | `int \| float` or `float` | | `boolean` | `bool` | | `array` | `list` | | `object` | `dict` | | `any` | `Any` | ## Inputs are native objects Upstream JSON is auto-parsed before your code runs. If the source node produced a JSON string, declare `dict` or `list`, not `str` — you get real Python objects, not strings. | Upstream output | Declare as | You get | | ------------------------ | ---------------- | --------------- | | JSON string `'{"a": 1}'` | `dict` | `{"a": 1}` | | JSON array `'[1, 2, 3]'` | `list` | `[1, 2, 3]` | | Plain text | `str` | The text string | | Number | `int` or `float` | The number | ## Examples ### Simple transformation ````markdown theme={null} ### filter-active Filter users to only active accounts. - type: code - inputs: users: ${fetch-users.response.data} ```python code users: list result: list = [u for u in users if u['status'] == 'active'] ``` ```` ### Multiple inputs ````markdown theme={null} ### merge-sources Combine data from two upstream nodes. - type: code - inputs: api_data: ${fetch-api.response} db_records: ${query-db.result} ```python code api_data: list db_records: list merged = api_data + db_records result: dict = { 'items': merged, 'count': len(merged), } ``` ```` Access fields downstream: `${merge-sources.result.items}`, `${merge-sources.result.count}`. ### No inputs ````markdown theme={null} ### generate-range Generate a sequence with no external inputs. - type: code ```python code result: list = list(range(1, 11)) ``` ```` ### Using subprocess ````markdown theme={null} ### get-git-log Fetch recent git history. - type: code - timeout: 60 ```python code import subprocess output = subprocess.run( ['git', 'log', '--oneline', '-10'], capture_output=True, text=True, check=True ).stdout result: str = output.strip() ``` ```` ### Imports Standard library imports work without restrictions — `json`, `re`, `subprocess`, `pathlib`, `datetime`. Third-party packages work too, but they need to be installed in pflow's environment. pflow runs in an isolated environment, so `pip install pandas` won't work. How you add packages depends on how you installed pflow: **pipx** (simpler — additive, doesn't touch existing packages): ```bash theme={null} pipx inject pflow-cli pandas ``` **uv tool** (replaces the full environment — list existing extras first): ```bash theme={null} # Check what's already installed so you don't lose it uv tool list --show-with # Include ALL extras — both new and existing uv tool install --with pandas --with llm-openrouter pflow-cli ``` `uv tool install` is not additive — it recreates pflow's environment from scratch. If you previously installed with `--with llm-openrouter` and now run `--with pandas` alone, you'll lose `llm-openrouter`. Always run `uv tool list --show-with` first and include all existing `--with` flags. A simpler way to manage code node dependencies is coming in a future release. ````markdown theme={null} ### parse-dates Parse and sort date strings. - type: code - inputs: raw_dates: ${fetch.response.dates} ```python code from datetime import datetime raw_dates: list parsed = [datetime.fromisoformat(d) for d in raw_dates] parsed.sort() result: list = [d.isoformat() for d in parsed] ``` ```` ## Security Code runs directly in the pflow process via `exec()` without sandboxing. It has full access to your filesystem, network, and standard library — same as running a Python script yourself. Sandboxed execution is planned for a future release. **You're in control.** Your agent asks before running workflows, and you can open any `.pflow.md` file to see exactly what code will run. To disable this node entirely: `pflow settings deny code`. ### External code file For larger scripts, reference an external Python file. The file is read and used as the code block. Path is relative to the workflow file. ```markdown theme={null} ### transform Transform data using external script. - type: code - code: ./scripts/transform.py - inputs: data: ${fetch.response} ``` ## Error handling Errors include line numbers, the actual source line, and a suggestion for how to fix it. For example, a type mismatch looks like: ``` Input 'data' expects list but received dict Suggestions: - Change the type annotation to: data: dict - Or convert the input value to list ``` | Error | Cause | What you see | | ------------------ | --------------------------------------- | -------------------------------------------- | | Missing annotation | Input or result has no type annotation | Which variable needs an annotation | | Type mismatch | Input value doesn't match declared type | Expected vs actual type, with fix suggestion | | `NameError` | Undefined variable | Variable name, suggestion to add to inputs | | `ImportError` | Module not installed | Module name, install command | | Timeout | Code exceeded time limit | Current timeout, suggestion to increase | | `SyntaxError` | Invalid Python | Line number from Python parser | | Runtime error | Any other exception | Exception type, line number, source line | The node doesn't retry — code execution is deterministic, so retrying the same code produces the same result. # File operations Source: https://docs.pflow.run/reference/nodes/file Read, write, copy, move, and delete files **Agent commands.** Your AI agent uses these nodes in workflows. You don't configure them directly. The file nodes read, write, copy, move, and delete files on disk. They handle both text and binary data with automatic encoding detection. For processing file contents — transforming data, extracting information — pipe the output to a code or LLM node. ## read-file Reads a file and stores its contents in the shared store. ### Parameters | Parameter | Type | Required | Default | Description | | ----------- | ---- | -------- | ------- | ---------------------------------------- | | `file_path` | str | Yes | - | Path to the file to read | | `encoding` | str | No | `utf-8` | Text encoding (ignored for binary files) | ### Output | Key | Type | Description | | ------------------- | ---- | ------------------------------------------------------------- | | `content` | str | File contents (with line numbers for text, base64 for binary) | | `content_is_binary` | bool | `true` if content is base64-encoded binary data | | `file_path` | str | Normalized absolute path that was read | | `error` | str | Error message (only present on failure) | ### Behavior **Text files** are returned with 1-indexed line numbers: ``` 1: First line of file 2: Second line of file 3: Third line of file ``` **Binary files** (images, PDFs, executables) are automatically detected and returned as base64-encoded strings. The node detects binary files by: * Known extensions: `.png`, `.jpg`, `.pdf`, `.zip`, `.mp3`, `.exe`, `.woff`, etc. * Encoding failures: Files that fail UTF-8 decoding fall back to binary mode **Path handling**: Expands `~` to home directory and converts to absolute path. *** ## write-file Writes content to a file, creating parent directories as needed. ### Parameters | Parameter | Type | Required | Default | Description | | ------------------- | ---- | -------- | ------- | --------------------------------------------------- | | `file_path` | str | Yes | - | Path where file should be written | | `content` | str | Yes | - | Content to write (text, JSON, or base64 for binary) | | `encoding` | str | No | `utf-8` | Text encoding | | `append` | bool | No | `false` | Append to file instead of overwriting | | `content_is_binary` | bool | No | `false` | Set to `true` if content is base64-encoded binary | ### Output | Key | Type | Description | | --------- | ---- | --------------------------------------- | | `written` | str | Success message with file path | | `error` | str | Error message (only present on failure) | ### Behavior **Atomic writes**: Regular writes use a temp file + rename pattern to prevent partial writes. Append mode writes directly. **Auto-serialization**: Dict and list content is automatically serialized to pretty-printed JSON. **Directory creation**: Parent directories are created automatically if they don't exist. **Disk space check**: Verifies sufficient disk space (2x content size) before writing. *** ## copy-file Copies a file to a new location, preserving metadata. ### Parameters | Parameter | Type | Required | Default | Description | | ------------- | ---- | -------- | ------- | ------------------------------- | | `source_path` | str | Yes | - | Source file path | | `dest_path` | str | Yes | - | Destination file path | | `overwrite` | bool | No | `false` | Overwrite if destination exists | ### Output | Key | Type | Description | | -------- | ---- | --------------------------------------- | | `copied` | str | Success message with paths | | `error` | str | Error message (only present on failure) | ### Behavior * Preserves file metadata (timestamps, permissions) using `shutil.copy2()` * Creates parent directories for destination automatically * Checks disk space (1.5x file size) before copying * Fails if destination exists and `overwrite` is `false` *** ## move-file Moves a file to a new location. ### Parameters | Parameter | Type | Required | Default | Description | | ------------- | ---- | -------- | ------- | ------------------------------- | | `source_path` | str | Yes | - | Source file path | | `dest_path` | str | Yes | - | Destination file path | | `overwrite` | bool | No | `false` | Overwrite if destination exists | ### Output | Key | Type | Description | | --------- | ---- | -------------------------------------------------------------- | | `moved` | str | Success message with paths | | `warning` | str | Warning if source deletion failed after copy (partial success) | | `error` | str | Error message (only present on failure) | ### Behavior * **Same filesystem**: Uses atomic rename * **Cross-device**: Falls back to copy + delete, preserving metadata * Creates parent directories for destination automatically * Handles partial success (copy succeeds, delete fails) with warning *** ## delete-file Deletes a file with required confirmation. ### Parameters | Parameter | Type | Required | Default | Description | | ---------------- | ---- | -------- | ------- | ---------------------------------- | | `file_path` | str | Yes | - | Path to the file to delete | | `confirm_delete` | bool | Yes | - | Must be `true` to confirm deletion | **Security exception**: The `confirm_delete` parameter must be set in the shared store (via a previous node or template variable). It cannot be provided directly in node parameters. This prevents accidental deletions from workflow configuration files. ### Output | Key | Type | Description | | --------- | ---- | ---------------------------------------------------- | | `deleted` | str | Success message (includes note if file didn't exist) | | `error` | str | Error message (only present on failure) | ### Behavior * **Idempotent**: Returns success if file already doesn't exist * **Safety**: Requires explicit confirmation via shared store * **Files only**: Cannot delete directories (use shell node for `rm -r`) *** ## Common patterns ### Chaining file operations ```markdown theme={null} ### read Read the input file. - type: read-file - file_path: input.txt ### process Summarize the file contents using an LLM. - type: llm - prompt: Summarize: ${read.content} ### write Write the summary to a file. - type: write-file - file_path: summary.txt - content: ${process.response} ``` ### Working with binary files ```markdown theme={null} ### read_image Read a binary image file. - type: read-file - file_path: photo.png ### copy_image Write the image to a backup location. - type: write-file - file_path: backup/photo.png - content: ${read_image.content} - content_is_binary: ${read_image.content_is_binary} ``` ## Error handling All file nodes return an `error` action on failure. Common errors: | Error | Cause | | ------------------- | ------------------------------- | | `FileNotFoundError` | File or directory doesn't exist | | `PermissionError` | Insufficient permissions | | `IsADirectoryError` | Expected file, got directory | | `OSError` | Disk full, filesystem issues | # HTTP Source: https://docs.pflow.run/reference/nodes/http Make API requests to web services **Agent commands.** Your AI agent uses this node in workflows. You don't configure it directly. The HTTP node calls APIs and web services. It handles JSON and binary responses, supports all standard methods, and has built-in auth — so your agent doesn't need to construct Authorization headers manually. ## Parameters | Parameter | Type | Required | Default | Description | | --------- | -------- | -------- | ------- | ---------------------------------------------------------- | | `url` | str | Yes | - | API endpoint to call | | `method` | str | No | Auto | HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) | | `body` | dict/str | No | - | Request payload (dict for JSON, str for raw) | | `headers` | dict | No | `{}` | Additional HTTP headers | | `params` | dict | No | - | Query parameters | | `timeout` | int | No | `30` | Request timeout in seconds | ### Authentication (mutually exclusive) | Parameter | Type | Description | | ---------------- | ---- | ------------------------------------------------------- | | `auth_token` | str | Bearer token for `Authorization: Bearer ` header | | `api_key` | str | API key for custom header | | `api_key_header` | str | Header name for API key (default: `X-API-Key`) | You cannot use both `auth_token` and `api_key` in the same request. ## Output | Key | Type | Description | | -------------------- | ----- | ----------------------------------------------------------- | | `response` | any | Response data (JSON parsed to dict, text, or base64 binary) | | `response_is_binary` | bool | `true` if response is base64-encoded binary | | `status_code` | int | HTTP status code | | `response_headers` | dict | Response headers | | `response_time` | float | Request duration in seconds | | `error` | str | Error description (only for non-2xx responses) | ## Method auto-detection If `method` is not specified: * **POST** if `body` is provided * **GET** if no `body` ## Response handling **JSON responses** are automatically parsed to dict/list. **Binary responses** (images, PDFs, etc.) are base64-encoded. Detected by Content-Type: * `image/*`, `video/*`, `audio/*` * `application/pdf`, `application/zip`, `application/octet-stream` **Text responses** are returned as strings. ## Examples ### GET request ```markdown theme={null} ### fetch Fetch users from the API. - type: http - url: https://api.example.com/users ``` ### POST with JSON body ```markdown theme={null} ### create Create a new user via POST request. - type: http - url: https://api.example.com/users - body: name: John email: john@example.com ``` ### With authentication ```markdown theme={null} ### fetch_protected Fetch data from a protected API endpoint. - type: http - url: https://api.example.com/private - auth_token: ${api_token} ``` ### With query parameters ```markdown theme={null} ### search Search the API with query parameters. - type: http - url: https://api.example.com/search - params: q: pflow page: 1 limit: 10 ``` Results in: `https://api.example.com/search?q=pflow&page=1&limit=10` ### Download binary file ```markdown theme={null} ### download Download a binary image file. - type: http - url: https://example.com/image.png ### save Save the downloaded file to disk. - type: write-file - file_path: downloaded.png - content: ${download.response} - content_is_binary: ${download.response_is_binary} ``` ## Error handling **HTTP errors** (4xx, 5xx) return the `error` action with details in `error` key. The response body is still available in `response`. **Network errors** trigger automatic retry (3 attempts, 1 second wait): | Error | Message | | ----------------- | ----------------------------------- | | Timeout | "Request timed out after X seconds" | | Connection failed | "Could not connect to URL" | | Other | "HTTP request failed: " | ## Status codes | Action | Condition | | --------- | --------------------------------- | | `default` | Status 2xx (success) | | `error` | Status 4xx/5xx or network failure | # Nodes overview Source: https://docs.pflow.run/reference/nodes/index Building blocks for pflow workflows **For reference, not memorization.** Your AI agent knows which nodes to use and how to configure them. This reference is for understanding what's possible. Nodes are the building blocks of pflow workflows. Each node performs a single operation - reading a file, calling an API, running a shell command - and passes data to the next node through the shared store. ## Core nodes pflow includes these built-in nodes: Read, write, copy, move, and delete files Call AI models with prompts and images Make API requests to web services Execute shell commands Execute Python for data transformation Repository-aware tasks via Claude or Codex Use tools from MCP servers ## How nodes work Every node follows the same pattern: 1. **Read inputs** from node parameters 2. **Execute** its operation 3. **Write outputs** to the shared store 4. **Return an action** that determines the next node Because every node declares its inputs and outputs, pflow can validate the whole workflow before anything runs — catching bad template references, missing fields, and type mismatches at build time. ### Parameters vs shared store Nodes receive data through parameters. Parameters can be static values or template variables that pull from the shared store: | Type | When to use | Example | | ---------------------- | ----------------------------------------------------- | ----------------------------------- | | **Static parameters** | Fixed values set when building the workflow | `"model": "gpt-4"` | | **Template variables** | Dynamic values from previous nodes or workflow inputs | `"prompt": "${summarize.response}"` | Template variables like `${node_id.key}` are resolved at runtime from the shared store and injected into node parameters. You can access nested fields and array elements directly: `${api.response.items[0].name}`. Nodes write their outputs to the shared store, making them available for template variables in subsequent nodes. See [Template variables](/how-it-works/template-variables) for complete syntax and examples. ### Automatic JSON parsing When a node outputs a JSON string and the next node expects an object, pflow automatically parses it. This means shell commands that output JSON work directly with other nodes — no extra conversion steps needed. ### Output keys Each node writes specific keys to the shared store. For example: * `read-file` writes `content`, `file_path`, `content_is_binary` * `llm` writes `response`, `llm_usage` * `http` writes `response`, `status_code`, `response_headers` * `code` writes `result`, `stdout`, `stderr` Check each node's documentation for its complete interface. ## Discovering nodes Your agent uses these commands to find the right nodes: ```bash theme={null} # List all available nodes pflow mcp list # Search by capability pflow mcp find "read a JSON file" # Get full interface details pflow mcp describe read-file ``` ## Extending with MCP Beyond core nodes, you can add capabilities from MCP servers. When you run `pflow mcp sync`, each MCP tool becomes a pflow node: ```bash theme={null} # Add a GitHub MCP server pflow mcp add github.mcp.json # Sync to discover tools pflow mcp sync github # New nodes appear pflow mcp list github # mcp-github-create_issue # mcp-github-list_repos # ... ``` See [MCP tools](/reference/nodes/mcp) for details on how this works. ## Node categories | Category | Nodes | Purpose | | --------- | -------------------------------------------------------- | ------------------------------------------- | | **File** | read-file, write-file, copy-file, move-file, delete-file | Local filesystem operations | | **LLM** | llm | AI model calls via any provider | | **HTTP** | http | Web API requests | | **Shell** | shell | System command execution | | **Code** | code | Python data transformation | | **Agent** | agent | Agentic repository work via Claude or Codex | | **MCP** | mcp-- | External tool integration | ## Disabling nodes You can disable any node (including core nodes) using the settings filter: ```bash theme={null} # Disable shell commands entirely pflow settings deny shell # Disable specific file operations pflow settings deny delete-file # Re-enable a node pflow settings remove shell ``` Disabled nodes won't appear in `pflow mcp list` and can't be used in workflows. See [settings commands](/reference/cli/settings) for details. # LLM Source: https://docs.pflow.run/reference/nodes/llm Call AI models with prompts and images **Agent commands.** Your AI agent uses this node in workflows. You don't configure it directly. The LLM node calls AI models using [LiteLLM](https://docs.litellm.ai/). Use it when a step needs reasoning — summarization, classification, extraction, anything a Python expression can't handle. It supports many providers (OpenAI, Anthropic, Google, OpenRouter, Ollama, and 100+ more) through a unified interface. ## Parameters | Parameter | Type | Required | Default | Description | | ---------------------- | ----- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | str | Yes | - | Text prompt, or path to an external file (e.g., `./prompts/system.md`) | | `model` | str | No | See below | Model identifier | | `system` | str | No | - | System prompt for behavior guidance | | `temperature` | float | No | `1.0` | Sampling temperature (0.0-2.0) | | `max_tokens` | int | No | - | Response-length ceiling. On reasoning models it caps thinking depth but never raises it — see [Reasoning depth](#reasoning-depth) | | `reasoning_effort` | str | No | - | `xhigh`/`high`/`medium`/`low`/`minimal`/`none` — how hard the model thinks | | `reasoning_max_tokens` | int | No | - | Explicit thinking-token budget (mutually exclusive with `reasoning_effort`) | | `images` | list | No | `[]` | Image URLs or file paths for vision models | | `output_schema` | dict | No | - | JSON Schema for structured output | | `prompt_cache` | list | No | `[]` | Names of `## Cache` chunks to include as a cached system prefix. See [Prompt caching](/how-it-works/prompt-caching) | | `prewarm` | bool | No | `false` | On batch nodes: make one short LLM call before the batch dispatches to warm up the provider's cache, so every item runs at cache-read prices. Warms both declared `## Cache` content (when `prompt_cache:` is set) and the fixed portion of the prompt template (the text before each `${item.X}` reference). | ### Model resolution If `model` is not specified in workflow params, pflow auto-detects based on your configured API keys. Most users just need an API key: ```bash theme={null} pflow settings set-env OPENAI_API_KEY "sk-..." ``` See [LLM model settings](/reference/cli/settings#llm-model-settings) for the full resolution order and default models per provider. ## Reasoning depth `reasoning_effort` controls how hard a reasoning model thinks. `max_tokens` controls how long the response may get. They're separate dials, and pflow keeps them that way: effort sets the thinking budget, and `max_tokens` only ever *caps* it — raising `max_tokens` to leave room for a longer answer never inflates reasoning spend. This matters because Anthropic counts thinking and answer against one `max_tokens` pool and rejects any request where the thinking budget isn't strictly smaller than `max_tokens`. pflow always derives the budget to sit under `max_tokens`, so that rejection can't happen — whether you set `reasoning_effort` or an explicit `reasoning_max_tokens`. If you set both `reasoning_max_tokens` and a smaller `max_tokens`, the budget is capped to fit (the explicit budget is a request, not a guarantee). One consequence on reasoning models: if you omit `max_tokens`, the provider may cap the *visible* answer low (LiteLLM defaults it to the thinking budget plus \~4096). Set `max_tokens` explicitly when you need a long answer from a reasoning model. The same `reasoning_effort` value maps to provider-specific knobs under the hood — Anthropic and Gemini 2.5 get a token budget, OpenAI and Gemini 3 get their native effort/level. Models without a reasoning knob ignore the parameter. ## Output | Key | Type | Description | | ----------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `response` | any | Text response (str), any parsed JSON value when `output_schema` is set, or the original raw text when structured-output validation fails | | `llm_usage` | dict | Token usage metrics | | `error` | str | Error message (only present on failure) | ### Token usage structure ```json theme={null} { "model": "openai/gpt-5.2", "input_tokens": 150, "uncached_input_tokens": 150, "output_tokens": 89, "total_tokens": 239, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "input_token_accounting": "total_includes_cache" } ``` ## Model support These providers are included with pflow - just set your API key: Always include the provider prefix in the `model:` field — bare names route inconsistently (Gemini bare names try Vertex; OpenAI bare names usually work but aren't future-proof). | Provider | Example models | | --------- | ---------------------------------------------------------------------------------------- | | OpenAI | `openai/gpt-5.2`, `openai/gpt-5.1`, `openai/gpt-4o` | | Anthropic | `anthropic/claude-opus-4-5`, `anthropic/claude-sonnet-4-5`, `anthropic/claude-haiku-4-5` | | Google | `gemini/gemini-3.0-pro`, `gemini/gemini-2.5-flash` | ```bash theme={null} # Set API keys (stored in ~/.pflow/settings.json) pflow settings set-env OPENAI_API_KEY "sk-..." pflow settings set-env ANTHROPIC_API_KEY "sk-ant-..." pflow settings set-env GEMINI_API_KEY "..." ``` ## Other providers LiteLLM is built into pflow and recognizes 100+ providers natively — no plugin install needed. Set the appropriate API key (or omit it for Ollama) and reference the model with its provider prefix. ### OpenRouter ```bash theme={null} pflow settings set-env OPENROUTER_API_KEY "sk-or-..." ``` ```markdown theme={null} ### summarize Summarize content using OpenRouter. - type: llm - model: openrouter/anthropic/claude-sonnet-4-5 - prompt: Summarize this ``` ### Ollama (local models) ```bash theme={null} brew install ollama ollama serve ollama pull llama3.2 ``` ```markdown theme={null} ### summarize Summarize content using a local model. - type: llm - model: ollama/llama3.2 - prompt: Summarize this ``` See the [LiteLLM provider list](https://docs.litellm.ai/docs/providers) for the full set of supported providers (Mistral, Bedrock, Azure OpenAI, Vertex AI, vLLM, and more). ## Image support For vision-capable models, pass image URLs or local file paths: ```markdown theme={null} ### describe Describe the contents of a photo. - type: llm - prompt: What's in this image? - model: openai/gpt-5.2 - images: ["photo.jpg"] ``` Supported formats: JPEG, PNG, GIF, WebP, PDF Images can be: * Local file paths: `photo.jpg`, `/path/to/image.png` * URLs: `https://example.com/image.jpg` ## Examples ### Basic prompt ```markdown theme={null} ### summarize Summarize the content from the previous step. - type: llm - prompt: Summarize: ${read.content} - model: openai/gpt-4o-mini ``` ### With system prompt ```markdown theme={null} ### translate Translate the input text to Spanish. - type: llm - system: You are a translator. Respond only with the translation. - prompt: Translate to Spanish: ${input.text} - temperature: 0.3 ``` ### Structured output Use `output_schema` to request JSON matching a schema. Provider mechanisms vary: some use constrained decoding, while others use tool calls with best-effort arguments. pflow validates the returned JSON locally before publishing it, so a non-conforming value fails at the LLM step instead of breaking a downstream template. ````markdown theme={null} ### extract Extract named entities from the document. - type: llm - prompt: Extract entities from: ${document.content} - temperature: 0 ```yaml output_schema type: object properties: people: type: array items: type: string places: type: array items: type: string required: - people - places ``` ```` For an object schema, `response` is a dict and downstream templates access fields directly: `${extract.response.people}`. Array and primitive schemas produce their corresponding parsed JSON values. JSON numbers must be finite; `NaN`, infinities, and overflow-to-infinity values are rejected as invalid JSON. If the provider returns invalid JSON or a value that does not match the schema, pflow keeps the original response text, sets `error`, and follows the step's error edge. Schemas may use references that resolve within the authored schema, including local fragments, anchors, nested IDs, and same-document absolute references. Missing or external references are rejected before the provider call; pflow never retrieves schemas from the network. For compatibility with providers that implement structured output through an internal tool, pflow also repairs one narrowly defined transport artifact: an otherwise invalid response shaped exactly as `{"json_tool_call": { ...valid result... }}`. A valid schema-authored object containing `json_tool_call` is never rewritten. After upgrading from a version without local schema validation, a pre-existing memo-cache entry can replay its old output until the default 24-hour TTL expires. Run with `--no-cache` to bypass that stale read and force a fresh validated call. Without `output_schema`, you can still get JSON by prompting for it. The template system auto-parses JSON strings when you use dot notation: `${extract.response.people}`. But the model may not always comply — `output_schema` is the reliable approach. ### Image analysis ```markdown theme={null} ### analyze Analyze the contents of a user-provided image. - type: llm - prompt: Describe the main elements in this image - model: openai/gpt-5.2 - images: ["${file_path}"] ``` ### External prompt file For long or reusable prompts, reference an external file instead of inlining. The file path is relative to the workflow file. Template variables (`${var}`) inside the file are resolved normally. ```markdown theme={null} ### analyze Analyze source code for issues. - type: llm - prompt: ./prompts/code-review.md ``` ## Error handling | Error | Cause | Solution | | ---------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Unknown model | Model ID not recognized | Run `pflow settings llm show` to see configured models, or check the [LiteLLM provider list](https://docs.litellm.ai/docs/providers) for supported model strings | | API key required | Missing credentials | Set with `pflow settings set-env _API_KEY ` or `export _API_KEY=...` | | Rate limit | Too many requests | Wait and retry automatically (built-in retry) | The node retries transient failures automatically (3 attempts, 1 second wait). # MCP tools Source: https://docs.pflow.run/reference/nodes/mcp Use tools from MCP servers **Agent commands.** Your AI agent uses this node in workflows. You manage the MCP servers that provide these tools with [`pflow mcp`](/reference/cli/mcp) commands. MCP (Model Context Protocol) lets you extend pflow with external tools. When you add an MCP server, its tools become pflow nodes — same validation, same error handling, same template access as built-in nodes. ## How it works 1. **Add an MCP server** with `pflow mcp add` 2. **Sync tools** with `pflow mcp sync` (or let auto-sync handle it) 3. **Tools become nodes** named `mcp-{server}-{tool}` 4. **Agent uses them** like any other node ```bash theme={null} # Add a GitHub MCP server pflow mcp add github.mcp.json # Tools are now available pflow mcp list github # mcp-github-create_issue # mcp-github-list_repos # mcp-github-search_code ``` ## Node naming MCP tools become nodes with the pattern `mcp-{server}-{tool}`: | Server | Tool | Node name | | ---------- | ------------- | -------------------------- | | github | create\_issue | `mcp-github-create_issue` | | slack | send\_message | `mcp-slack-send_message` | | filesystem | read\_file | `mcp-filesystem-read_file` | ## Parameters MCP node parameters come directly from the tool's input schema. Each MCP server defines its own tools with their own parameters. To see a tool's parameters: ```bash theme={null} pflow mcp describe mcp-github-create_issue ``` ## Output All MCP nodes write to: | Key | Type | Description | | -------- | ---- | ------------------------------- | | `result` | any | Tool execution result | | `error` | str | Error message (only on failure) | For dict results, read nested fields through `result`: If a tool returns `{"issue_url": "...", "issue_number": 123}`, use: ```markdown theme={null} ${github-issue.result.issue_url} ${github-issue.result.issue_number} ``` MCP nodes do not extract top-level result fields into `${node.field}` and do not write a `{server}_{tool}_result` alias. Each workflow node already has its own namespace, so `${node.result}` is the canonical success output. ## Example workflow ```markdown theme={null} ### create_issue Create a GitHub issue from the error summary. - type: mcp-github-create_issue - repository: myorg/myrepo - title: "Bug: ${error_summary}" - body: ${error_details} ### notify Send a Slack notification with the issue link. - type: mcp-slack-send_message - channel: "#alerts" - text: "Created issue: ${create_issue.result.issue_url}" ``` ## Setup To use MCP tools, first add MCP servers to pflow. See [Adding MCP servers](/guides/adding-mcp-servers) for configuration format, authentication, and examples. pflow supports both local (stdio) and remote (HTTP) MCP servers. Both work identically in workflows - same naming, parameters, and output structure. ## Error handling MCP nodes return `error` action on: * **Connection failure**: Server not reachable * **Tool error**: Tool reported an error (via `isError` flag) * **Timeout**: Request took too long (default 30 seconds) The node does **not** retry automatically - MCP calls start a server subprocess, and retries would cause resource conflicts. ## Auto-sync When you run a workflow, pflow automatically syncs MCP tools if the server configuration has changed. You don't need to manually run `pflow mcp sync` after adding servers. Manual sync is useful for: * Testing connection immediately after adding a server * Debugging connection issues * Forcing re-discovery without running a workflow # Shell Source: https://docs.pflow.run/reference/nodes/shell Execute shell commands **Agent commands.** Your AI agent uses this node in workflows. You don't configure it directly. The shell node runs commands with pipes, redirects, and standard shell features. It blocks dangerous patterns (fork bombs, recursive deletes) before execution, but otherwise commands run with full system access. ## Parameters | Parameter | Type | Required | Default | Description | | --------------- | ---- | -------- | ----------- | ------------------------------------------------------------------------------ | | `command` | str | Yes | - | Shell command, or path to an external script file (e.g., `./scripts/build.sh`) | | `stdin` | any | No | - | Input data for the command | | `cwd` | str | No | Current dir | Working directory | | `env` | dict | No | `{}` | Additional environment variables | | `timeout` | int | No | `30` | Maximum execution time in seconds | | `ignore_errors` | bool | No | `false` | Continue workflow on non-zero exit | ## Output | Key | Type | Description | | ------------------ | ---- | ------------------------------------------------------- | | `stdout` | str | Command output (UTF-8 or base64 if binary) | | `stdout_is_binary` | bool | `true` if stdout is base64-encoded | | `stderr` | str | Error output (UTF-8 or base64 if binary) | | `stderr_is_binary` | bool | `true` if stderr is base64-encoded | | `exit_code` | int | Exit code (0=success, -1=timeout, -2=execution failure) | | `error` | str | Error message (only on timeout/failure) | ## Using stdin for data **Always use `stdin` for data, not command interpolation.** Shell escaping breaks on special characters in JSON, and `stdin` handles any data type safely. **Correct** - data via stdin: ````markdown theme={null} ### process Process the API response with jq. - type: shell - stdin: ${api.response} ```shell command jq -r '.data.name' ``` ```` **Wrong** - data in command (will break on special characters): ````markdown theme={null} ### process Process the API response with jq. - type: shell ```shell command echo '${api.response}' | jq ``` ```` ### stdin type handling | Input type | Conversion | | ---------- | --------------------------------- | | str | Used as-is | | dict/list | Serialized to JSON | | int/float | Converted to string | | bool | Lowercase string (`true`/`false`) | | bytes | Decoded UTF-8 (fallback: latin-1) | ## Validation and error handling **Your agent handles this.** pflow validates commands when the workflow is created, not at runtime. If structured data ends up in a command string, the error message tells your agent exactly what to move to `stdin` and why. The shell node validates that dicts and lists aren't embedded directly in command strings (which would break shell parsing). Data should go through `stdin` instead: ````markdown theme={null} ### process Process the API response with jq. - type: shell - stdin: ${api.response} ```shell command jq -r '.data.name' ``` ```` When a workflow is created, pflow checks if command templates contain dict or list variables. If found, you'll see an error like: ```` Shell node 'process': cannot use ${api.response} (type: object) in command parameter. PROBLEM: Object data embedded in shell commands breaks shell parsing. Dict/list data contains special characters (quotes, braces, spaces) that break command syntax. FIX: Move data to stdin, keep command simple: - stdin: ${api.response} ```shell command jq '.field' ``` ```` This validation happens at workflow creation time (compile-time), not during execution, so you get immediate feedback. **Why this matters:** Shell parsers expect text, and JSON data contains special characters (`{`, `}`, `"`, spaces) that have meaning to the shell. Even with careful quoting, it's fragile. The `stdin` approach is safer and more reliable. ## Security Commands run directly on your system without sandboxing. While dangerous patterns are blocked, the shell node has full access to your filesystem and network. Sandboxed execution is planned for a future release. **You're in control.** Your agent asks before running workflows, and you can inspect the workflow to see exactly what commands will execute. If you need stronger restrictions, you can disable shell entirely with `pflow settings deny shell` - but this significantly limits pflow's capabilities since shell is often used for data processing and filtering. ### Blocked patterns These commands are rejected immediately with an error: * `rm -rf /` and variants (recursive system deletion) * `dd if=/dev/zero of=/dev/sda` (device operations) * `:(){:|:&};:` (fork bombs) * `chmod -R 777 /` (dangerous permissions) * `sudo rm -rf /` (privileged dangerous commands) ### Warning patterns These trigger warnings but execute unless `PFLOW_SHELL_STRICT=true`: * `sudo`, `su -` * `shutdown`, `reboot`, `halt` * `systemctl poweroff` ## Smart error handling Some commands return non-zero exit codes for valid "not found" results. The shell node treats these as success: | Pattern | Exit code | Reason | | ----------------------- | --------- | -------------------------- | | `ls *.txt` (no matches) | 1 | Empty glob is valid | | `grep pattern file` | 1 | Pattern not found is valid | | `which nonexistent` | 1 | Command check | | `command -v foo` | 1 | Existence check | ## Examples ### Basic command ````markdown theme={null} ### list List files in the temp directory. - type: shell ```shell command ls -la /tmp ``` ```` ### Process JSON with jq ````markdown theme={null} ### fetch Fetch data from the API. - type: http - url: https://api.example.com/data ### extract Extract item names from the response. - type: shell - stdin: ${fetch.response} ```shell command jq -r '.items[].name' ``` ```` ### With environment variables ````markdown theme={null} ### deploy Run the deployment script. - type: shell - env: ENV: production DEBUG: "false" - timeout: 120 ```shell command deploy.sh ``` ```` ### Working directory ````markdown theme={null} ### build Build the project. - type: shell - cwd: /path/to/project ```shell command npm run build ``` ```` ### Ignoring errors ````markdown theme={null} ### cleanup Remove temporary log files. - type: shell - ignore_errors: true ```shell command rm -f temp/*.log ``` ```` ### External script file For multi-line scripts, reference an external file. The file content is read and used as the command. Path is relative to the workflow file. ```markdown theme={null} ### deploy Run the deployment script. - type: shell - command: ./scripts/deploy.sh ``` ## Error handling | Exit code | Meaning | | --------- | ---------------------- | | 0 | Success | | 1+ | Command-specific error | | -1 | Timeout | | -2 | Execution failure | The node returns `error` action on non-zero exit (unless `ignore_errors` is `true` or it's an auto-handled pattern like grep). ## Recommended tools pflow's template variables handle most data access - you can use `${api.response.items[0].name}` to access nested fields and array elements directly without shell commands. Shell is needed when you need to: * **Iterate over arrays** - `jq '.items[].name'` (templates can't do wildcards) * **Filter or transform** - `jq 'select(.active)'`, `sort`, `uniq` * **Compute values** - `wc -l`, arithmetic Common Unix tools are built into macOS: `grep`, `awk`, `cut`, `sort`, `head`, `tail`, `curl`. For JSON processing, install [jq](https://jqlang.github.io/jq/): ```bash theme={null} brew install jq ``` # Roadmap Source: https://docs.pflow.run/roadmap pflow's direction and priorities ## Current status Where pflow is today (v0.15.1): * **Standalone orchestration engine** with compile-once batch optimization (\~7x speedup for large parallel workflows) * **Markdown workflows** — `.pflow.md` files that agents read and write naturally * **Node system** — file, llm, http, shell, code (Python), agent (Claude or Codex), and MCP bridge * **Conditional branching** — `on-error`, static routing, and data-driven routing via code nodes with branch convergence (`??` coalesce operator) * **Nested workflows** — saved or file-based workflows as nodes inside other workflows, with automatic input/output mapping * **Memoization cache** — unchanged nodes serve cached results across re-runs, with `--only`, `--no-cache`, and per-node `cache: false` controls * **Execution reports** — `--report` generates navigable markdown directories with rendered prompts, responses, token breakdowns, and cost data per node * **Dry-run execution plans** — `--dry-run` previews cost, duration, and cache boundaries before execution * **Workflow visualization** — `pflow mermaid` generates Mermaid flowcharts of workflow topology, and `pflow ui` serves an interactive React Flow canvas with a live execution overlay, run controls, agent-directed focus, and voice narration * **Human-in-the-loop approval gates** — pause a workflow before a step with `approval: required`, answer paused gates from the CLI or browser canvas, and handle agent-raised escalations with auditable decisions * **Resume and durable gate pause** — `pflow resume` continues a failed, interrupted, or gate-paused run from where it stopped without re-running completed steps; a gate hit in a non-interactive run (CI, MCP, pipe, browser) pauses durably — exit 4 with a resume token — and resumes hours later with `--approve`/`--choose`, with `pflow resume list` showing pending pauses * **Stateful loops** — the `loop:` modifier runs a node repeatedly with `while:`/`until:` conditions and a `carry:` block to thread state across iterations * **Streamable traces** — execution traces stream to disk as JSONL for crash-tail resilience, with per-node retry/backoff via the `retry:` block * **Batch processing** — process arrays through nodes, sequential or parallel, with per-item parameter overrides and error handling modes * **External file references** — `prompt: ./prompts/system.md` keeps long content out of workflow files * **Workflow bundling** — `pflow save` packages workflows with all file dependencies as self-contained folders * **Unified diagnostics** — one error format across CLI text and JSON output, with structured suggestions and source file provenance * **Recursive sub-workflow validation** — structural errors caught before any node executes * **Template variables** — `${var}` syntax with nested path access, automatic JSON parsing, and structured output schemas * **AI agent integration** via CLI and MCP server * **Discovery** — find nodes and workflows by describing what you need * **Unix-first piping** — stdin/stdout, works with any Unix tool * **Skills publishing** — save workflows as Claude Code skills, cross-platform * **Settings management** — API keys, node filtering * **Unified model support** — 100+ providers (OpenAI, Anthropic, Google, OpenRouter, Ollama, ...) via LiteLLM * **Published on PyPI** — `uv tool install pflow-cli` * **Cross-platform execution** — macOS, Linux, and Windows support ## Now **Iteration speed and workflow quality** * **Function-based code node syntax** — write Python functions instead of top-level scripts, with automatic input/output wiring from type annotations * **Workflow export** — export a workflow to standalone Python with zero pflow dependency. Build and iterate with structure, ship plain code. * **Workflow testing** — mock nodes, assert outputs, `pflow test`. Modify a saved workflow and know it still works before re-publishing. * **Code and shell linting** — catch syntax errors in code blocks during validation, not at runtime * **Batch limits** — cap iteration count for development and cost control ## Later **Security and sandboxing** * **Sandboxed execution runtime** — isolated execution for shell and code nodes. Needed before running agent-generated workflows you haven't reviewed. * **Export as MCP server packages** — distribute workflows as standalone MCP servers that work without pflow installed **MCP ecosystem** * **MCP gateway integration** — route to remote MCP servers * **Dynamic MCP discovery** — search and install MCP servers on demand instead of manual configuration * **OAuth for remote MCP servers** — authenticate with HTTP-based MCP servers ## Vision pflow is infrastructure, not a destination. It provides building blocks and a runtime — agents do the assembly. The better the building blocks get, the more capable the agents become. The longer-term direction: * **Code node dependency management** — install packages on demand for code nodes * **TypeScript code node** — for teams that think in TypeScript * **Reduce/fold for batch** — aggregate batch results incrementally instead of collecting all at once pflow improves through a direct loop: agents build workflows, we find where they struggle, we fix it. Because the surface area is finite — node types, template syntax, error messages — each fix is targeted and compounds. A year from now pflow will be meaningfully better at helping agents build workflows, not because of some grand vision but because each friction point gets filed down one at a time. ## Get involved Ideas and feature requests Bug reports Guides and reference ***

Built by a developer who got tired of watching agents re-think the same tasks.
Questions or ideas? Reach out — [andreas@pflow.run](mailto:andreas@pflow.run)