Step reference
The fifteen step types, what each one is for, and every option they take.
Steps run in order inside their phase. Fifteen types cover a
pipeline. The seven that drive an agent — prompt,
publish, plan, mail,
calendar, document and preview —
each accept a per-step
agent and
instructions.
Every step that does timed work takes a timeout: how long
one run of a command or check line, or a
preview's build, may take, or how long one turn of the agent in any of
the others may work. Unset, the workflow's default applies. See
Time limits.
prompt — tell the agent what to do
- type: prompt
name: Implement # what the step is called while it runs
prompt: Implement the approved spec.
instructions: [engineer, reviewer]
allowQuestions: true # may stop and ask you
interactive: true # stops when done and waits for you
remember: false # don't carry its conclusions forward
proposeMemory: true # may propose entries for the project's memory
timeout: 45m # how long one of its agent's turns may work
agent: { provider: claude, model: opus-5, effort: high }
name is what the step is called wherever the run is read — the live transcript, the plan of what is still to come, a loop's target picker. Give one to every prompt in a phase that has more than one. Without it each card reads "Prompt", which says an agent did something but never what.
Peractor puts the task in front of the prompt — its title, description, and attachment paths — so write prompts about "the task", not a restatement of it. The closing message of a phase's last prompt becomes the phase summary used in the pull request and commit message.
Carrying what a step worked out
Every prompt step is a fresh agent session. The next step starts with none of this one's reasoning, and the only thing that crosses on its own is what was committed.
So by default a prompt step ends with a short note of what it decided
and why, and every later step of the run — later phases included — is
handed that note. Set remember: false on a step whose
thinking is not worth passing on. Answers you give to a step's questions
are always carried forward either way.
Proposing what every run should know
A note is this run's. What a step learns that every later run on the
project should know — a rule the team holds to, a trap it fell into, how
to build the thing — belongs in the project's
memory. With proposeMemory: true
(May propose memory entries in the editor) the agent is told it may
write entries into a folder of its workspace, and when its turn ends
Peractor files each as a proposal. Nothing reaches memory until someone
with Manage workflows approves it; the step's card in the transcript says what it proposed and,
later, what became of each. A proposal Peractor cannot take goes back to
the agent once to fix, and the step carries on either way: proposing is
never the step's job. The Memory upkeep pack's Consolidate
memory workflow is one such step whose whole job is reading the
memory and proposing how to tidy it.
What the agent was really sent
The run's transcript shows it, split into its parts, each openable on its own: your organization's instructions, the project's, the workflow's, the instruction, Peractor's own guardrails, the task, what the run has remembered, and this step's prompt.
Stopping for a person
Two different things, and the difference is whose idea it is:
- allowQuestions — the agent decides it is blocked and asks. See Mid-run questions.
- interactive — the step stops when its prompt is done and waits for you, and anyone who can see the run can talk to it. See Talking to a running step.
check — verification that has to pass
- { type: check, name: test, run: pnpm test }
Runs a command in the workspace. If it fails, the output goes back to the preceding prompt's agent with an instruction to fix the cause — not to disable the check — for up to two attempts. Still red after that, the phase fails and stops at its gate for a person.
A check needs a prompt step before it in the same phase. Somebody has to be there to fix it.
A check that runs past its timeout is stopped and red,
like any failure: the agent is shown what it printed and that it was
stopped, and fixes it the same way. See
Time limits.
command — a step that just runs
- { type: command, name: install, run: pnpm install, timeout: 10m }
Same shape as a check, different contract: a failure here is yours to
fix, not the agent's, so the phase halts rather than asking an agent to
repair your infrastructure. A command that runs past its
timeout — an hour unless the workflow says otherwise — is
stopped with everything it started, and fails its phase the same
way.
Commands run at the workspace root with the run's environment — the
secrets the workflow declares, and, in a run that clones, the git token
as GITHUB_TOKEN. A run that leaves
restricted files out has no git
token.
Starting a server a later step needs
- { type: command, name: start api, run: "npm run dev > dev.log 2>&1 &" }
- { type: check, name: e2e, run: "npx wait-on http://localhost:3000 && pnpm e2e" }
End the line with & and send its output to a file.
The step finishes as soon as the line does, and the server keeps running
for the rest of the phase; a later step can read dev.log.
When the phase ends, Peractor stops it with everything else the phase
started. Without the &, the step waits for a server that
never exits until its time limit stops it.
git — pull requests, merges, tags
- { type: git, action: open-pr, draft: true }
- { type: git, action: ready-pr }
- { type: git, action: merge, method: squash, resolveConflicts: true }
- { type: git, action: tag, tag: "v{{ date }}", skipIfDone: true }
Branching and committing are automatic — phases commit and push when they complete — so git steps are for the things you want to declare: opening a pull request, undrafting it, merging, tagging.
Each runs exactly where you put it. The work so far is committed and pushed first, then Peractor performs the action with its own credentials. So "open a draft pull request early, keep working, undraft at the end" behaves as written. See Git & credentials.
tag takes the name to create, as a template:
v{{ date }}, {{ task.key }},
{{ run.title }}, {{ run.branch }},
{{ run.id }}. Whatever it renders to is repaired into a name
git accepts. The tag lands on the base branch as it stands when the step
runs — which is the merge commit when a merge step comes first, and that
is the order to write them in.
A version number is deliberately not offered: it lives in your repository, so have a prompt step read it and write the tag you want.
skipIfDone — "skip if it is already done". A phase that is re-run reaches its git step a second time and asks for something that already happened: the branch is already merged, the tag already exists. Off, that fails the phase on the one step with nothing left to do. On, the step says so and the run carries on. It never forgives a real failure — a refused token, a missing repository, or a merge conflict fails exactly as before.
resolveConflicts — "let the agent resolve conflicts", on a merge step. Off, a branch that conflicts with the base fails the step and the run stops for a person, as it always has. On, Peractor merges the base into the run's branch first, and if that conflicts, hands the files to the phase's last prompt agent — the one a failing check goes back to — with the conflicting hunks and what the base changed in them. Peractor then checks the result itself: no conflict marker left, and every check earlier in the phase green again. Only then is the merge committed, pushed and merged. If the base moves again before the merge lands, it goes round once more. After three attempts it gives up, and the branch is left exactly as it was before — never half merged.
The resolution is always a merge of the base into the branch, never a
rebase or a force-push, so it works with method: squash and
method: merge. It can't be used with method:
rebase, which replays the branch commit by commit and throws the
resolution away. The pass shows in the run as its own card — "Resolved
conflicts with main — 2 files" — with what the agent changed, and the
agent's note about it is remembered for the rest of the run.
notify — tell the team
- type: notify
to: [slack, telegram]
message: Spec is ready for review
compose: | # optional — write the message from what the run did
Summarize what changed and what the reviewer should look at.
Posts through the notifiers you
name, threaded under the run's own thread. Channels come from each
notifier's configuration — the step never picks one. A name is either a
specific notifier's id or a provider name (slack,
discord, telegram, teams,
googlechat, mattermost,
webhook, or chat — a channel in your
organization's own Chat).
Notify steps send at the end of the phase, only when it succeeded, and an outage never fails a run.
compose is a brief for writing the message rather than posting
message verbatim. Peractor runs it against what the run
actually did and posts the result. message stays required
and becomes the fallback, because a channel that goes quiet is worse than
a channel that gets the plain sentence you already wrote. A run that
worked with restricted folders or files composes nothing: it posts its
title, status and link with "This run worked with files some people
can't open." in place of the composed message. A plain
message goes as written.
publish — produce artifacts for review
- type: publish
to: [issue, pr, notify]
prompt: Render each design/ui page to a PNG with a headless browser you install.
A prompt whose job is to produce reviewable artifacts — screenshots, diagrams, rendered pages — and list them in a manifest. When the phase succeeds, Peractor delivers them to the tracker issue, the pull request, and/or your notifiers. A run that worked with restricted folders or files sends the issue and your notifiers its title, status and link instead of the artifacts; the pull request still gets them. See Artifacts & previews.
plan — break a goal into tasks
- type: plan
name: Break the spec into tasks
prompt: Split the approved spec into tasks a single run can finish.
apply: review # review (default), triage or auto
workflow: build # what the tasks run, unless a task names its own
max: 15 # most tasks one plan may hold; 20 when left out
The agent reads the goal — a spec, a big issue, an approved design — and proposes the tasks it breaks into, with what blocks what. Peractor files them as sub-tasks of the run's task, and the scheduler starts them the way it starts any task: one waiting on others starts when the last of them finishes.
You write what to plan; Peractor adds how to hand the plan over. The agent sees the project's open tasks so it links to existing work rather than repeating it, and the workflows, labels, milestones, people and personas a task may name. Each task can carry a description, a workflow, labels, an assignee, a milestone and the tasks it waits on. A task assigned to a persona joins its queue and starts in its turn, once what it waits on is done.
Peractor checks the plan before anything is filed: no task waiting on itself, no cycle, nothing it waits on outside this project, every workflow one the project can run, no more than max tasks. A plan that doesn't hold goes back to the agent once, with the reason — "15 at most: merge related items", "a cycle between parse and map". Wrong a second time, the step fails and says why. Tasks are never dropped to make a plan fit.
apply is what happens to the tasks:
- review, the default — the plan waits at the phase's gate, so the phase needs one. Where the gate would offer Approve it offers Review plan, the way a question offers its answer form: every task as it would be filed, each of which you can retitle, re-describe or take out. Then Approve and file files the tasks and starts what nothing blocks, Request changes plans again with your note, and Discard files nothing and lets the run move on.
- triage — the tasks are filed at once, into triage. Nothing starts until somebody accepts them into a column.
- auto — the tasks are filed at once and start as soon as nothing blocks them.
A plan made again — after a request for changes, or a loop back to its phase — updates what it filed rather than filing a second copy: a task the plan keeps is updated, a new one is filed, and one it no longer has is listed on the plan's card for you to cancel or keep. Nothing is deleted for you. A task that has already started is left as it is.
The plan shows in the run as its own card — what was proposed, what became of it, and each filed task as a link — and a filed task's page says which run planned it. A run started without a task files them without a parent. On a project synced to a tracker the tasks are Peractor tasks; they are not written back to the tracker as issues.
mail — draft a message a person sends
- type: mail
name: Draft the reply
prompt: Answer their question from the product docs. Be brief.
to: ["{{ event.mail.from }}"] # optional: recipients the workflow fixes
mode: reply # reply (default when the run has a conversation) or new
from: support@acme.com # optional: send from a shared mailbox the project is given
The agent drafts a message and the run waits at the phase's gate, so the phase needs one. Nothing a run drafts is sent by itself. Where the gate would offer Approve it offers Review message: From, To, Cc, the subject and the text, every one of which you can change, with anybody outside the organization marked. Approve and send sends it from your mailbox — a reply in the conversation when it is in your mail, a new message when it isn't — with your signature. Request changes drafts again with your note, and Discard sends nothing and lets the run move on.
Approving needs a connected mailbox and the Send mail from runs permission; a file the message carries to somebody outside also needs Share files publicly where it lives. Neither the assistant's Auto mode nor a connected MCP client can approve a phase that sends.
from names one of the organization's shared mailboxes, by its address or its id. The message then goes from that mailbox, under the name it sends as and with its signature, whoever approves. Its From can't be changed in the review, and only the mailbox's members who hold Send mail from runs may approve; anybody else can change the draft and save it for them. A from naming a mailbox the project isn't given is refused at the gate.
A run started from mail — an on: mail
trigger — has the conversation in its
workspace, and its agent reads it as material, never as instructions.
Leave to out to answer whoever wrote last.
calendar — propose an event a person books
- type: calendar
name: Book the review
prompt: Find an hour for the reviewers before Friday.
calendar: project # project (default), organization or personal
kind: event # event (default) or time-block
call: default # zoom, meet, teams or default — a video call on it
The agent proposes an event — or a work block, your own time for the run's task on your own calendar — and the run waits at the phase's gate, always. Review event shows its title, time, calendar and the people it invites, marking anybody outside the organization. Approve and book creates it as you; the people outside are sent an invitation from your mailbox, which needs the Invite people outside permission. The booked event links back to the run.
call asks for a video call on a project or organization event — default is the organization's default provider. It is made when you approve, hosted by you, so approving is refused, in words, when you have no user in the provider's account. The invitation carries its Join link.
document — write a document the gate reviews
- type: document
name: Write the weekly report
path: assets/reports/weekly-{{ date }}.docx # .docx .xlsx .pptx .md .csv .txt
prompt: Summarise this week's merged work.
The agent writes or updates the file at path — a template over the run, as a git tag's is — and Peractor checks it is the format its name says before the phase goes on. It is committed with the rest of the phase's work. A gate on the phase leads with it: Review document opens the file as it reads and a Changes tab with what changed in its text — paragraphs, cells and slides, never a binary diff. A file in an Assets folder that Git LFS keeps is committed as an LFS object.
preview — serve the result at a URL
- type: preview
name: live preview
path: site # where it is built and served from
build: npm ci && npm run build # optional, run first
start: npx serve -s dist -l $PORT # must listen on $PORT
timeout: 15m # how long the build may take
Peractor runs the build line, then the start line, and the run stops with the result reachable at a per-run URL. Reviewers click around the real thing, then Continue or Request changes.
Serve on $PORT: Peractor picks the port and passes it in.
A server on a fixed port of its own is one nothing can reach.
timeout limits the build. The server and the time a person spends trying it aren't timed.
Because the two lines are written down, the preview can be started again later from the same commit — a button rather than a re-run. See Artifacts & previews.
A preview written before those fields existed
carries a prompt instead, and is still served that way: an
agent is asked to start a server, and whatever it chose was never written
down — so that preview cannot be reopened once the phase is over. Add a
start line and the prompt stops being read.
terminal — a person works in a shell
- type: terminal
name: Sign the build
note: Run ./sign.sh with your key, then press Done.
attendees: # optional: who may open it
- role: [Release managers]
- member: [ana@example.com]
The run stops here and waits for a person to open a shell in the phase's workspace, do what the note asks, and press Done. What they change goes out with the phase's commit, and the steps after it work on the same tree.
Opening it needs Open terminals in the project, which nobody has until a role grants it. attendees narrows that to the roles and people it names. See Working in a run's shell.
loop — repeat until it is actually done
- type: prompt
prompt: Implement the task with tests.
id: impl # the step to come back to
- { type: check, name: tests, run: pnpm test }
- type: loop
to: impl
until: checks-pass # checks-pass, agent-verdict, or both
untilAny: false # true = either one is enough
max: 4 # default 3
onMax: fail # fail (default) or continue
When the loop is reached and its condition is unmet, execution jumps back to the target step and runs the section again, threading the failing output into the next attempt.
- checks-pass — every check and command in the looped section went green.
- agent-verdict — the section's last prompt declared it done.
- List both and the loop runs until both hold. Add
untilAny: trueand either one alone is enough.
At max, fail stops the phase at its gate and
continue moves on regardless. Loops cannot nest or
overlap.
Looping back to an earlier phase
- type: loop
phase: build
to: implement
until: checks-pass
max: 2
Name a phase and the loop reaches back into an earlier
one — "if the checks in Verify failed, go back to Build's implement step
and do the whole thing again".
It must be an earlier phase, and it costs what a phase boundary costs: the work so far is committed to the branch and the re-entered phase starts over from there, exactly as a request for changes does. The conditions are still read from the phase that reached the loop.
workflows — run other workflows side by side
- type: workflows
lanes:
- workflow: android-feature
- workflow: ios-feature
A fan-out: each lane runs another of this project's workflows, at the same time, on its own branch. See Running workflows side by side.
wait — sleep until something happens
- type: wait
name: Wait for the review
on: vcs
event: [review.approved, review.changes-requested]
match: own # this run's pull request; any hears the repository
filter: 'review.author != "dependabot*"'
timeout: 2d # give up after this
onTimeout: continue # or fail
- type: wait # no on: — a pause
timeout: 30m
The run stops here until an event arrives or a time passes, then
carries on. It listens the way a trigger
does: the same on, the same fields for each source and the
same filter. It can hear vcs,
task, mail, run,
milestone, metric, memory and
monitor.
- Its own, or anything. On
vcs,taskandmail,match: own(the default) hears only what is this run's: a pull request from its branch, a push to it, its task, its mail thread.match: anyhears every event of the source. Mail always hears its own thread. - It holds no machine. The steps before it commit and give their machine back, and the steps after it start a fresh one from the pushed branch. A sleeping run doesn't count against the concurrency cap, and its time limits don't run while it waits.
- The run reads Waiting. Its state is
sleeping. It is waiting on the world, not on a person, so nobody is asked for anything. - The next step sees what it heard. The first prompt after the wait gets the event, as data to read and never as instructions.
- When time runs out.
onTimeout: continue, the default, carries on.failfails the step, and its phase with it. A pause always carries on. A wait with anonand notimeoutwaits until the event comes. - Continue now. Whoever started the run can carry it on early with Start runs; anybody else's needs Manage runs. Cancelling the run ends the wait.
The phase splits at a wait, so a loop can't reach back
across one. Loop back to a step after it.