workflows/, at any depth - px0 loads *.md recursively. The YAML frontmatter is the machine contract; the body is the prompt the model receives. That is the whole format - there is no compile step, no lock file, and no hidden state. Edit the file and the next run uses it.
Frontmatter fields
description and request are not the same thing, and the distinction matters when you come back to a workflow months later. request is your words; description is the model’s normalization of them.
Watching instead of scheduling
trigger.watch fires on something happening rather than on the clock. px0 polls the named read-only tool at every (at least 60s), identifies each item by key (or by its own id, url, or number if no key is named), and runs the workflow when something new turns up:
Retries and failure notifications
retry controls how many times a failed run is attempted before it is recorded as failed for good, and how long to wait between attempts, doubling each time. Each attempt still writes its own run record, so px0 runs list shows the failures that led to an eventual success rather than hiding them.
on_failure decides how you hear about it: a local desktop notification, a tool call such as slack.post_message or gmail.send_message, or nothing. A workflow’s own on_failure block wins over the store-wide notify.* config, which is what lets a noisy hourly job stay quiet while a nightly report shouts. See Configuration.
Inputs
Inputs run before the prompt and their results are interpolated into it. Think of them as the context-gathering phase: by the time the model reads the body, the data is already there. Each input has anid and exactly one of four sources:
tool - call a read tool
tool - call a read tool
inputs is a validation error, because inputs run unconditionally - a workflow should not post something just by starting up. Put write tools in tools instead, where the model decides whether to call them.args values are themselves templated, so an input can build on an earlier one.retrieve - query your brain
retrieve - query your brain
brain/ and interpolates the matching passages, each prefixed with its path#anchor so the model can cite them. k defaults to retrieval.k_default (5).Anything under brain/work/ is excluded from this, as it is from every other retrieval px0 performs.source - read piped input
source - read piped input
px0 workflows run <id> --stdin. stdin is the only supported source.workflow - run another workflow first
workflow - run another workflow first
Optional inputs
optional: true and a failure instead resolves to nothing, the run continues, and the record marks it as degraded so you can see afterwards that something was missing.
Templating
The body and everyargs value are templated. Reference an input by its id:
args are rendered against the context built so far, so a later input can use an earlier one: args: {query: "{{topic}}"}.
Dotted lookups work for structured values and for the two built-in namespaces:
Guideline inlining
Guidelines named inguidelines: are read and inlined verbatim at run time. If the body contains {{guidelines}}, they go exactly there; otherwise they are prepended before the body.
This is deterministic on purpose. Guidelines are matched by name, never retrieved by similarity, because a convention you rely on should not depend on a search hit. The price is that an irrelevant guideline in the list costs tokens and misleads the model, which is why the builder attaches one only when it genuinely matches the task.
The run record notes which guideline files were inlined and at which version, so a run whose output looks off can be compared against the conventions in force at the time.
Output
path, and the reason for all of them is that the path can come from a model-written plan rather than from you:
- It is always confined under
output/. An absolute path, or a..that climbs out, is rejected at run time rather than written. Everything else is resolved relative to the store’s output directory. - Only three placeholders are supported:
{date},{datetime}, and{time}. Both{date}and{{date}}styles work, since a plan that picked up the body’s{{...}}habit would otherwise produce a filename with literal braces in it. Any other placeholder is an error, not a filename. The default whenpathis omitted isoutput/output-{date}.md. - Concurrent writes are serialized with a store-wide lock, so two runs targeting the same path cannot interleave.
A workflow with
trigger.schedule must use target: file. Nobody is watching stdout when a scheduled run fires, so px0 rejects that combination at validation time instead of dropping the output.Pipelines
A workflow can be a sequence of other workflows instead of a prompt:output block. Each stage still produces its own run record alongside the pipeline’s.
A stage that fails aborts the pipeline, and the run is recorded as failed. Pipelines cannot nest - a stage that is itself a pipeline is a validation error, which keeps the execution graph one level deep and legible.
Validation
Every run validates the file first, and the builder validates the plan before saving it. These are the checks, and each one exists because the alternative is a confusing failure much later:A broken file does not break the rest
A workflow whose frontmatter does not parse is skipped, not fatal. One YAML typo used to take down every workflow command; now the other workflows keep working, and the broken file is reported by name and line - YAML’s own error reports the position as<unicode string>, which is no help at all.
Editing by hand
Editing the file directly is expected and supported. The daemon’s nightly pass checkpoints your edits into version history, so they appear alongside px0’s own changes:px0 workflows edit re-derives the tools, inputs, and guidelines from your revised request and keeps them consistent. See Build a workflow.

