Skip to main content
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

Output

stdout and stderr hold what your code prints, kept separate per batch item. Output from threads your code starts itself (threading.Thread, ThreadPoolExecutor, loop.run_in_executor) is not captured — it goes to pflow’s own streams — so return data from threads through result. Don’t wrap calls in contextlib.redirect_stdout to silence them: prints are already captured, and the redirect swaps the process-wide stream, which misroutes output when batch items run in parallel.

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.
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.
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

Template variables go in inputs, never in the code block. The code block is plain Python: pflow never fills in ${var} there, and each inputs key becomes a Python variable. A ${var} in a string that names a value pflow knows about ("${name}".upper()) fails validation, and the message names the inputs binding to use.
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.

Inputs are native objects

Upstream JSON object and array text is parsed before your code runs. If the source node produced a JSON string, declare dict or list to get real Python objects — or str to get the text exactly as produced. A workflow input declared with a type other than any is never parsed: it arrives as its declared type. Binding a - type: string input to a dict or list annotation is a validation error — declare the input object or array instead.

Examples

Simple transformation

Multiple inputs

Access fields downstream: ${merge-sources.result.items}, ${merge-sources.result.count}.

No inputs

Using subprocess

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):
uv tool (replaces the full environment — list existing extras first):
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.

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. Inside the file, __file__ is its absolute path, so Path(__file__).parent finds files next to it from any working directory. Relative paths in the code itself (open("data.json")) still resolve against the working directory. An if __name__ == "__main__": block in the file does not run — the file is a step body, not the program’s entry point.

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:
By default the node doesn’t retry — code execution is deterministic, so retrying the same code produces the same result. A step’s retry: re-runs a body that raises or calls sys.exit.