Problem
The cwd field explicitly rejects all interpolation placeholders ({args.*}, {steps.*}) at validation time, while nearly every other field supports them. There is no safe alternative form (unlike run, which has an array escape hatch).
Which fields support {args.*} / {steps.*} interpolation?
| Field |
Interpolation |
Notes |
task |
✅ |
|
context |
✅ |
{steps.X} refs + file paths |
input (script stdin) |
✅ |
|
over (map) |
✅ |
|
when |
✅ |
|
expect |
✅ |
|
def / with (flow) |
✅ |
|
until (loop) |
✅ |
|
run (string form) |
❌ |
Rejected — shell injection guard |
run (array form) |
✅ |
Safe alternative: ["python3","x.py","{args.y}"] (execvp, no shell) |
cwd |
❌ |
Rejected — no alternative provided |
Source
schema.js line 42-43, 464-465:
/** `cwd` is a literal path / workspace keyword, not an interpolated field. */
const CWD_PLACEHOLDER_RE = /\{[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\}/;
if (typeof p.cwd === "string" && CWD_PLACEHOLDER_RE.test(p.cwd)) {
errors.push(`Phase '${p.id}': 'cwd' does not support interpolation placeholders (${p.cwd}). Use a literal path, or a reserved workspace keyword ('temp', 'dedicated', 'worktree').`);
}
At runtime, runtime.js passes phase.cwd directly to spawn({cwd: ...}):
function resolveEffCwd(deps, phase) {
return deps._cwdOverride
?? (isWorkspaceKeyword(phase.cwd) ? deps.cwd : phase.cwd ?? deps.cwd);
}
Node.js spawn accepts relative paths (resolved against the parent process cwd), so a runtime-resolved {args.repo_dir} would work fine — the issue is purely the validator blocking it.
Impact
Users whose flow needs a dynamic working directory (e.g. multi-repo monorepo where repo_dir is passed as an arg) cannot use cwd: "{args.repo_dir}". They must write an external render script that pre-processes the flow JSON, replacing {args.repo_dir} with a literal path before calling taskflow run.
This is the only field that requires such a workaround with no in-DSL escape hatch, which is surprising and adds friction.
Reproduction
action: "verify" → error:
Phase 'work': 'cwd' does not support interpolation placeholders ({args.repo_dir}).
Use a literal path, or a reserved workspace keyword ('temp', 'dedicated', 'worktree').
Meanwhile task in the same phase interpolates {args.repo_dir} just fine.
Proposal
Allow {args.*} (and ideally {steps.*}) interpolation in cwd, resolving placeholders before passing to spawn().
The run field's design is instructive: it blocks string-form interpolation (shell injection risk) but provides the array form as a safe alternative. cwd has no equivalent escape hatch — args are author-declared values (not untrusted user input), so allowing {args.*} at minimum would eliminate the most common workaround.
If full interpolation is too risky, consider a dedicated cwdArgs or allowing cwd to accept an object form like { "from": "args", "key": "repo_dir" } as a safe, bounded alternative.
Workaround
External render script that reads {args.repo_dir} and replaces it with a literal path before taskflow run. Works but adds a step and a script to maintain.
Environment
pi-taskflow: installed via file: link from packages/pi-taskflow
taskflow-core: local file dependency
- pi-coding-agent: 0.80.3
Problem
The
cwdfield explicitly rejects all interpolation placeholders ({args.*},{steps.*}) at validation time, while nearly every other field supports them. There is no safe alternative form (unlikerun, which has an array escape hatch).Which fields support
{args.*}/{steps.*}interpolation?taskcontext{steps.X}refs + file pathsinput(script stdin)over(map)whenexpectdef/with(flow)until(loop)run(string form)run(array form)["python3","x.py","{args.y}"](execvp, no shell)cwdSource
schema.jsline 42-43, 464-465:At runtime,
runtime.jspassesphase.cwddirectly tospawn({cwd: ...}):Node.js
spawnaccepts relative paths (resolved against the parent process cwd), so a runtime-resolved{args.repo_dir}would work fine — the issue is purely the validator blocking it.Impact
Users whose flow needs a dynamic working directory (e.g. multi-repo monorepo where
repo_diris passed as an arg) cannot usecwd: "{args.repo_dir}". They must write an external render script that pre-processes the flow JSON, replacing{args.repo_dir}with a literal path before callingtaskflow run.This is the only field that requires such a workaround with no in-DSL escape hatch, which is surprising and adds friction.
Reproduction
{ "name": "demo", "args": { "repo_dir": { "default": "packages/api" } }, "phases": [ { "id": "work", "type": "agent", "cwd": "{args.repo_dir}", "task": "Do work in {args.repo_dir}" } ] }action: "verify"→ error:Meanwhile
taskin the same phase interpolates{args.repo_dir}just fine.Proposal
Allow
{args.*}(and ideally{steps.*}) interpolation incwd, resolving placeholders before passing tospawn().The
runfield's design is instructive: it blocks string-form interpolation (shell injection risk) but provides the array form as a safe alternative.cwdhas no equivalent escape hatch —argsare author-declared values (not untrusted user input), so allowing{args.*}at minimum would eliminate the most common workaround.If full interpolation is too risky, consider a dedicated
cwdArgsor allowingcwdto accept an object form like{ "from": "args", "key": "repo_dir" }as a safe, bounded alternative.Workaround
External render script that reads
{args.repo_dir}and replaces it with a literal path beforetaskflow run. Works but adds a step and a script to maintain.Environment
pi-taskflow: installed viafile:link frompackages/pi-taskflowtaskflow-core: local file dependency