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: true and 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, task and mail, 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: any hears 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. fail fails the step, and its phase with it. A pause always carries on. A wait with an on and no timeout waits 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.