Workflow Actions
Use an action for deterministic file, repository, or tool work. Use an agent role for tasks
that need judgment. Actions await a result; they cannot declare expect or call
prowl workflow deliver. They are separate from the app’s shell-command Custom Actions.
Package layout#
report.pwlworkflow/
workflow.yaml
actions/
summarize-files/
action.yaml
main.py
helpers.py
Use action: local:summarize-files for that package or action: builtin:collect-worktree-context for Prowl’s
repository collector. Local IDs use lowercase kebab-case (up to 64 ASCII characters). There is no global script registry. Helpers,
schemas, and assets must live inside the workflow bundle. Symlinks and special files are
rejected. Pass the bundle directory to prowl workflow validate, not workflow.yaml.
A script declaration:
schema: prowl.action/v1
name: Summarize files
input_schema:
type: object
properties:
paths: {type: array, items: {type: string}}
required: [paths]
additionalProperties: false
output_schema:
type: object
properties:
count: {type: integer}
required: [count]
additionalProperties: false
backend:
type: script
interpreter: python3
entrypoint: main.py
arguments: []
inherit_env: []
timeout: 30s
The interpreter is a literal executable name resolved through Prowl’s PATH, or an absolute executable path. The entrypoint is relative to its action directory and cannot escape it. Arguments are literal strings. Schema defaults do not insert missing inputs. Input and output roots must be objects, validated with JSON Schema Draft 2020-12, without network schema fetching.
Request and result#
The script receives one JSON object on stdin:
{
"protocol": "prowl.action/v1",
"input": {"paths": ["README.md"]},
"context": {
"action": {
"execution_id": "<execution UUID>",
"step_id": "summarize",
"attempt": 1,
"working_directory": "/repo",
"artifacts_directory": "/Users/example/.prowl/logs/workflow-runs/repo-<hash>/2026-09/<run>/actions/summarize/<execution>/artifacts"
}
}
}
context also contains the workflow’s step snapshot; see authoring.
The working directory is the selected worktree. Write exactly one schema-conforming JSON
value to stdout; put diagnostics on stderr. Exit zero is necessary for success.
import json
import sys
request = json.load(sys.stdin)
json.dump({"count": len(request["input"]["paths"])}, sys.stdout)
The workflow supplies typed values:
- id: summarize
action: local:summarize-files
with:
paths: [README.md, CHANGELOG.md]
- id: report
notify: "Found {{ actions.summarize.output.count }} files"
A complete {{ expression }} retains its JSON type in with; interpolation in larger
text accepts scalars only. Results appear at actions.<step>.output and the invocation’s
JSON file at actions.<step>.output_path. Retain results in typed state before leaving a
branch or loop iteration if later steps need them.
Approval and testing#
Scripts run with the local user’s permissions. Review their code, helpers, and assets in Settings > Agents > Workflows > Review Bundle…, then approve that exact version. Approval covers the canonical source location and every file’s content. Moving the bundle or editing any file requires review again. Approval does not start a run. CLI commands do not grant approval. Start-time inputs and changing repository contents do not alter the grant.
Prowl copies the approved definition into each run and checks its integrity before actions. Editing the source affects future runs; editing the run copy invalidates the current run. Cancel an invalidated run and start a newly reviewed version.
After validation and approval, test one action through the normal runner:
prowl workflow test-action report local:summarize-files --input-json '{"paths":["README.md"]}' --json
prowl workflow status <run-id> --json
The first argument is a discovered workflow ID/name. An optional source selects the target
worktree as for workflow run. Tests create real runs and have real side effects. Then run
the whole workflow to verify expressions, result scopes, and agent handoffs together.
Each attempt uses a new UUID under actions/<step>/<execution>/, with request.json,
result.json on success, execution.json, bounded stdout.log/stderr.log, and artifacts/. Inspect these
records after failures; raw stdout is retained even for invalid JSON or nonzero exit.
Raw logs have the same local access as the request/result records. Manual retry can repeat side effects; no automatic retry or rollback
is promised. Cancel/timeout terminates the owned script process group. It does not undo work
already done or stop independent agent tasks.
Default timeout: 30 seconds. Input and stdout: 16 MiB each. Stderr: 4 MiB. The JSON request envelope has separate transport headroom. JSON depth: 64.
Bundle: 64 MiB and 8192 entries. Interpreter environment starts with PATH, HOME, TMPDIR, LANG,
and LC_ALL when present. backend.inherit_env names extra inherited variables; PROWL_*
control variables are always stripped. Prowl sets PYTHONDONTWRITEBYTECODE=1 so Python
helper imports do not add cache files to the fixed bundle. Environment values are not
recorded in the request.
Collect worktree context#
builtin:collect-worktree-context takes optional root, restricted to the selected worktree, and returns
output.path plus output.branch. It writes the Markdown repository summary into this
invocation’s artifacts/context.md. It does not write shared handoff files.
prowl workflow schema --action exports the manifest schema. Schema references can use
local JSON or YAML files in the same bundle. Network references and $id overrides are
rejected; use local $ref paths and anchors. --input-json supplies literal JSON data,
so strings containing {{ are not evaluated as workflow expressions.
When a script bundle needs approval, the workflow start screen provides Review Bundle… and keeps Run disabled. Approval returns to the same start screen; it does not start a run.
The current collector requires a Git directory and collects that directory only. Its
worktree-oriented name describes the workflow target; multi-repository workspace and
plain-directory collection are future work. builtin:collect-agent-context is planned,
not registered. Use verb-first kebab-case names for local actions, such as
local:write-report; the runner does not infer behavior or permissions from the name.
Save handoff#
builtin:save-handoff takes a required briefing path to a UTF-8 briefing file within the
current run directory, normally {{ deliveries.briefing.path }}. It validates the briefing
before updating shared handoff state. It supports the workflow handoff storage targets:
Git worktrees, workspaces, and plain directories. Available source session context is included.
The action returns output.path (an independent briefing/context packet),
output.current_path, and output.context_path. Packets live under
.prowl/handoff/archive/ and survive workflow history cleanup. The action never launches an
agent. The built-in prowl.handoff combines this action with a current-agent briefing and
an optional receiver launch. Retrying a save creates a new packet and may archive shared
state again; cancellation does not roll back completed writes.
Assert condition#
builtin:assert-condition accepts a required boolean condition and nonempty string
message. Use a boolean literal or a complete expression template such as
condition: '{{ inputs.min_rounds <= inputs.max_rounds }}'. A true condition returns
an empty output object. False requests attention with the supplied message and does
not advance. Invalid types fail; strings such as "true" are not booleans.
For invalid start inputs, cancel and start with corrected values; retry keeps the
original inputs. This action needs no script interpreter or bundle approval.