Tasks
Approve, and each task becomes a child item keyed ENG-2101/api, down the queue like any item, on its own branch away/ENG-2101-api, opening its own PR. The card makes no unit of its own anymore.
- A task matches only rules whose top-level
wherenamesplanned, the first whoseskillis the task's; such a rule takes nothing else. Caps, attempts,skip, gates and chains work on tasks as on anything. - Tasks without
afterrun in parallel;afterorders them, and a card's tasks run back to back, as one block. - A task carries the card's source, URL, owner, fields and whole thread, and its repo unless the plan names another, so
where: { planned: true, labels: backend }works.
Its item.md is awaywork's own, the task first:
# ENG-2101/api: VAT export endpoint
https://acme.atlassian.net/browse/ENG-2101
Add GET /exports/vat to src/routes/exports.ts, paged like /exports/sales.
## The plan
This is task `api` of the plan you approved on ENG-2101. [...]
Your notes on approving it:
Leave voided invoices out.
## The card: ENG-2101, Export VAT report
[...]Every run reads the plan off the card and each task's PR off the forge (the PR on the task's branch whose footer names the task, open, merged or closed). awaywork plan --skipped shows the card, run by run:
| What happened | The card | Its tasks |
|---|---|---|
| You approved | fanned out: api no PR yet, web waits for api | api runs and opens its PR; web is skipped as waiting for task api's PR to merge |
api's PR is open | fanned out: api PR open, web waits for api | api waits for your review, under Needs you, like any awaywork PR |
api said continue | fanned out: api draft says continue, web waits for api | the next window run goes on with api's draft |
api merged | fanned out: api merged, web no PR yet | web runs, cut from the default branch, which has api in it |
web's PR closed unmerged | plans again | none run until you approve the new plan |
| Every task done | all tasks done: close or move the card, under Needs you | none: each merged, or a report task posted its done result |
A task's PR says Part of <card>, never closes the card, and its footer names only the task, so the card never links its tasks' PRs or folds into one.
The plan sticks to the card
Once awaywork's plan comment is on an open card of yours, the card goes to the deliver: plan rule it matches, else the first for its source whatever its where says now, and no other rule takes it whole. plan --rules lists it there, with (plan) after its key when the where no longer holds. So a planned rule's on_done: { move: In Progress } can move the card off a plan rule on status: To Do and its tasks still run, where a catch-all implement lower down would otherwise take the card as one unit.
- It leaves the plan flow when closed or reassigned (read every run);
awaywork skipon the card holds it and all its tasks. - Someone else's edit to the card's only plan comment makes it not awaywork's: back to ordinary matching.
- With no
deliver: planrule left for its source, a catch-all may take the card whole, next to its open task PRs.
Task details
- Questions from a task go on the card, headed
Questions from task api(awaywork's questions from task apiinitem.md), through the usual ownership check. The run stops the card's block there: no task of the card runs after it that run, skipped asapi's questions wait for your answer, so one post of a card waits for you at a time. While they wait, later runs skip every task of the card aswaiting for your answer to task api's questions. The first comment after them that's neither a bot's nor awaywork's answers them. - A task's PR is its result: it posts nothing else on the card. A report task posts its report there instead.
- Writes go to the card: a
plannedrule'son_donemoves the card; a task writes nowhere else. Reassign the card and nothing fans out or writes (plan approved, but the card isn't yours: its tasks don't run). - A task PR closed unmerged after the plan was posted re-plans the card, though the card didn't change. Its id is retired: the prompt names it and its PR, with the tasks whose PR is open or merged, and a plan reusing it isn't posted. The new plan needs a fresh approval, even under
wait: false; a task that keeps its id keeps its PR. - A task's branch is its name for good. When something else holds
away/ENG-2101-api, the task is skipped withdelete it to run this task, never moved to-2, which its PR lookup wouldn't find. - A
branchtemplate renders from the task's key alone, so a re-plan that retitles a task keeps its PR:{title}or{type}in aplannedrule's template, own or inherited, is a config error. A re-plan may hand a task another skill, and so anotherplannedrule, so allplannedrules for a source set the samebranch, or none does. - A task branch whose PR failed to open still reads
no PR yet(the PR is the task's state), and each morning report saysaway/ENG-2101-api exists on acme/api without a PR; delete it to run again. - A task takes no approval of its own, the plan's covers it: no
approved,awaywork approve tracker:ENG-2101/apiis refused, and aplannedrule takes nowait. The one exception is a report task's result that a gate of its rule fired on: it waits for your reply. - On Bitbucket it works the same, through twg. A declined or superseded PR counts as closed unmerged, when last updated; a stacked task's PR goes into the branch of the one below (no stacks there); a task whose draft says
continue, or asks, holds its dependents back, but it's yours to finish.
Report tasks
For proof of the change, a write-up or a prototype link, give the skill a planned rule that reports, and the plan can hand it a task:
queue:
- skill: plan
where: { label: awaywork-feature }
deliver: plan
- skill: implement
where: { planned: true }
deliver: pr
- skill: prove
where: { planned: true }
deliver: reporttasks:
- { id: app, title: Export screen }
- { id: qa, title: Proof, skill: prove, after: { app: opened } }Its result is its report, posted on the card when its chain finishes, as on_done: { comment: result } would, unasked: Result of task qa, the report, Status: done (awaywork's result of task qa in item.md). Like any awaywork comment, it answers nobody's questions, a plan's included, and isn't a change to the card.
- It's done once a
doneresult for it, newer than your approval, is on the card, and no gate holds it:planskips it asdone: its result is on <card>, and the card readsqa done. Until then it runs, failed attempts counting as usual. A re-plan's approval leaves older results behind, so its tasks report again. - A
continueresult isn't done: the task runs again. - Nothing can wait on it: no PR to stack on, check or merge, so a plan whose
afternames one isn't posted. It can wait on others, asqawaits forapp. - A
plannedrule deliversdraft_pr,prorreport; anything else is a config error.
A gate on a report task
Give the rule gates of its own to check what the task left, say that every acceptance criterion has a file:
queue:
- skill: prove
where: { planned: true }
deliver: report
gates:
proof: { command: sh scripts/evidence-gate.sh } # reads $AWAYWORK_OUT_DIROne that fires posts the result as a post that waits, the gate's line on top:
Result of task qa
## Needs a decision
### Gates (from scripts)
- proof: no file for criterion 3
---
Two of three criteria recorded against the preview.
Status: done
To approve, reply with approve as the first line. Any other reply is feedback: awaywork runs this step again with it.
— awaywork · waits- It holds the feature, as a task's questions do. The task isn't done:
planskips it as waiting for your approval (approve,task qa's result), under Needs you, and every other task of the card aswaiting for your approval of task qa's result on <card>. The card readsfanned out: api waits for qa, qa waits for your approval. - One post of a card waits at a time. The run that posts it stops the card's block: no task of the card runs after it that run, skipped as
qa's result waits for your approval. Results the tasks before it posted stay as they are. - Your first reply after it decides, from your own account; a teammate's comment is context. First line
approve: you accept the gap, and the task is done.awaywork approve tracker:ENG-2101/qa, the card's key oraposts it. Anything else is feedback, and ends the hold too: the card readsqa runs again on your feedback, the task runs again with the thread initem.md, ahead of the tasks after it, and the other tasks go on. Its gates run on the new result: if one fires again that run, the tasks after it stop behind it. A pinned file of the post edited in the store is feedback too, whatever you replied, though the card didn't change. - None fired:
Gates: none fired (proof)tops the result, marked— awaywork · went ahead · gates=proof, and the task is done as without gates. - A newer plan leaves the result behind:
approve tracker:ENG-2101/qais refused asolder than the plan, and while that plan waits your reply answers the plan. - A
plannedrule'son_done: { move }is held with the post, as on any that waits. - All of it is read off the card or its thread every run.
The feature block
after says what a task waits for:
merged: run after run, until that task's PR merges; then it's cut from the default branch, which has it. Each PR lands and is reviewed before the next starts.opened: only until that task's PR opens, in the same run. Its clone starts from that task's branch and its PR stacks on that task's, so the whole feature is up for review in the morning, as a stack. A task in another repo only waits.- a check's name, like
Vercel Preview:openedplus that check passing, which takes a later run (Waiting on a check).
The run that sees a card runs its tasks back to back, as one block, in the order they wait on each other, before the next unit, wherever the first ranks. A task joins once every merged task it names merged and every opened one has an open or merged PR, or runs earlier in the block. So a block stops only where it needs you: at merged, to merge something, or at a task's post that waits, its questions or a result a gate fired on, to answer it.
tasks:
- id: api
title: VAT export endpoint
- id: web
title: Export button
after: { api: opened }The run after approve:
Units 2
item rule skill provider deliver title
1 ENG-2101/api #2 implement (built-in) fake pr VAT export endpoint
2 ENG-2101/web #2 implement (built-in) fake pr Export button
after api, stacks on its PR
Skipped 1: 1 fanned_outapi opens away/ENG-2101-api; web is cloned from it and its PR goes into it, the body ending Stacked on <api's PR>: merge that first. Once api's PR is open, a later run stacks the same way, and plan --why ENG-2101/web shows after api opened (PR #41 open) and stack after api, stacks on #41.
When the task before opens no PR (it failed, asked or committed nothing), those stacked on it are skipped as api opened no PR. When it never ran (setup failed, Ctrl-C, the window's end, or run --unit named only the task after it), they're skipped as api didn't run, and those on them as web didn't run.
| What happened | The card | Its tasks |
|---|---|---|
| You approved | fanned out: api no PR yet, web no PR yet | api opens its PR, then web runs on api's branch and opens its PR, stacked on api's |
api failed | fanned out: api no PR yet, web no PR yet | web is skipped as api opened no PR; both run again next run |
api said continue | fanned out: api draft says continue, web no PR yet | web is skipped as api left a draft that says continue; the window run that finishes api runs web right after it, stacked on its PR. A check on api's PR waits as its draft says continue |
A cap cut the block after api | fanned out: api PR open, web no PR yet | the next run stacks web on api's open PR: after api, stacks on #41 |
api merged before web ran | fanned out: api merged, web no PR yet | web is cut from the default branch |
| Both PRs are open | fanned out: api PR open, web PR open | both wait for your review |
Stacking details
- One chain. A task's
openedtasks, check ones too, lie on one chain, each after the one before, and it stacks on the last. Withapiafter{ schema: opened },webafter{ api: opened, schema: opened }stacks onapi, which holdsschema. Off one chain there's nowhere to stack, so the plan isn't posted:task web: its opened tasks api, schema aren't on one chain: make each after the one before, e.g. schema after api: opened. - A merged PR in the stack: a task stacks on the first unmerged PR down the stack. With
api's PR merged intoschema's branch,webafter{ api: opened }stacks onschema(after api, stacks on schema's #10), cut from the default branch only once the whole stack merged. mergedandopenedmix:mergedsays when it can start,openedwhere.webafter{ schema: merged, api: opened }waits aswaiting for task schema's PR to merge, then stacks onapi.mergedmeans on the default branch: ifapistacks onschemaand merges into its branch first,webafter{ api: merged }waits aswaiting for task schema's PR to merge, api's merged into it.- A task waiting on an
openedtask that doesn't run (waiting for an answer, unchanged, gave up, cut by a cap) is skipped aswaiting for task api's PR to open. Caps and the window's end can cut a block; the next run carries on from the task with no PR. - No secrets in a stacked setup. A stacked clone starts on a branch a worker wrote, so
setupgets none of the repo'ssecrets, as on any awaywork PR a rule takes up again (Safety). Ifsetupneeds one (a registry token forpnpm install, say), usemerged, notopened, or every stacked task fails setup. - Merge the stack bottom-up. Once
api's PR merges and its branch is deleted, GitHub retargetsweb's to the default branch; awaywork does nothing. After a squash mergewebstill carriesapi's commits: merging the default branch into it usually resolves cleanly.
Across repos
A task's repo can be any repo repos lists, by name or by glob; unset, it's the card's. A card with no repo needs one on every task. The plan step can also write a contract, the interface the tasks agree on, and a task lists in ships the files its PR will hold that other repos' tasks need, like the spec a client is generated from:
contract: .away/out/openapi.yaml
tasks:
- id: api
title: VAT export endpoint
ships: [openapi.yaml]
- id: web
title: Export button
repo: acme/web
after: { api: opened }The contract goes in the plan comment before plan.yml, under a heading naming it, in a fence longer than any backtick run in it (so a Markdown contract with fences survives):
VAT export spans two repos: the endpoint in acme/backend, then the button in acme/web.
### Contract: openapi.yaml
```yaml
openapi: 3.1.0
[...]
```
```yaml
contract: .away/out/openapi.yaml
[...]
```You approve it with the plan. Every task finds it in .away/context/<task>/contract/openapi.yaml, beside item.md, its prompt saying it was approved.
Each task is a unit in its own repo: clone, setup, secrets, test, branch template, gates and units_per_repo are that repo's, and its PR opens there. Questions and on_done still go to the card.
opened across repos only waits: with no branch to stack on, web is cut from acme/web's default branch, which no agent wrote, so its setup gets the repo's secrets. It joins the block once api opened its PR earlier in the run, else it's skipped as api opened no PR:
Units 2
item rule skill provider deliver title
1 ENG-2101/api #2 implement (built-in) fake pr VAT export endpoint
2 ENG-2101/web #2 implement (built-in) fake pr Export button
in acme/web
waits for api's PR in acme/backendweb gets what api ships in .away/context/<task>/shipped/api/openapi.yaml, named in its prompt, read as web is about to run from api's PR head, open or merged. A missing file skips web as api's PR has no openapi.yaml, every run until it's there, counting no attempt.
Details:
- Any other repo isn't posted:
task x: repo evil/x isn't the card's (acme/backend) or one the config names. Case counts, since a repo's settings and caps go by its exact name:Acme/Web isn't acme/* as configured. - The contract counts toward the 30 KB unless the plan is lean, must read back as the file it was, and your edits to it are checked on every read. One the file names but the step didn't write isn't posted; an edit that drops it from the comment refuses the plan.
- The one-chain rule counts only
openedtasks in the task's own repo: withuiinacme/webtoo,webafter{ api: opened, ui: opened }stacks onuiand waits forapi. - A task in the same repo gets no
shipped/: a stacked one has the files in its branch, amergedone is cut from a default branch holding them. - On Bitbucket a shipped file is read through twg (
twg bb repo file), at the PR's branch or, once merged, its head commit.
Waiting on a check
The button wants the endpoint deployed to try against. Vercel, Netlify and Expo report previews as checks on the PR, so name the check in after, exactly as the PR shows it:
tasks:
- id: api
title: VAT export endpoint
ships: [openapi.yaml]
- id: web
title: Export button
repo: acme/web
after: { api: Vercel Preview }Anything in after but merged and opened is a check's name, matched letter for letter: opened, plus that check's latest run or status on the PR's head commit succeeded (on GitHub a neutral or skipped check run passes, as for a required check). A merged PR counts as passed. Until then the task waits:
$ awaywork plan --skipped
Needs you 1
ENG-2101/api: waiting for review: awaywork PR https://github.com/acme/backend/pull/41 is open
Skipped 2: 1 fanned_out, 1 waiting_for_task
ENG-2101: fanned out: api PR open, web waits for api's check Vercel Preview
ENG-2101/web: waiting for api's check Vercel Preview (pending)The check reads pending, failing, not reported (no check by that name: check the spelling) or no PR yet. A failing one keeps waiting: a fix rule may fix it, or you re-plan. It never passes in the run that opened the PR, so web runs the first run after the preview is up, cut from acme/web's default branch with api's openapi.yaml, shown as after api's check Vercel Preview (passed). While the PR is open, its item.md plan has the check's link too, after api's check Vercel Preview (passed, https://…), which is whatever reported the check: a skill that opens it names the host it must be on.
Details:
- In the same repo a check's name stacks like
openedand counts toward the one chain; in another repo it only waits and gets what the task ships. - awaywork reads a PR's checks only when a task names one, so other plans cost no extra calls; a failed read skips the card.
plan --jsonhas them undertaskPrs.api.checks, each with itsstateandurl. - On Bitbucket, checks are the build statuses on the PR's head commit, by name, via
twg bb prs get --statuses:SUCCESSFULpasses,INPROGRESSis pending,FAILEDandSTOPPEDare failing. A name carrying a build number never matches.