Skip to content

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 where names planned, the first whose skill is the task's; such a rule takes nothing else. Caps, attempts, skip, gates and chains work on tasks as on anything.
  • Tasks without after run in parallel; after orders 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:

markdown
# 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 happenedThe cardIts tasks
You approvedfanned out: api no PR yet, web waits for apiapi runs and opens its PR; web is skipped as waiting for task api's PR to merge
api's PR is openfanned out: api PR open, web waits for apiapi waits for your review, under Needs you, like any awaywork PR
api said continuefanned out: api draft says continue, web waits for apithe next window run goes on with api's draft
api mergedfanned out: api merged, web no PR yetweb runs, cut from the default branch, which has api in it
web's PR closed unmergedplans againnone run until you approve the new plan
Every task doneall tasks done: close or move the card, under Needs younone: 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 skip on 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: plan rule 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 api in item.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 as api'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 as waiting 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 planned rule's on_done moves 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 with delete it to run this task, never moved to -2, which its PR lookup wouldn't find.
  • A branch template renders from the task's key alone, so a re-plan that retitles a task keeps its PR: {title} or {type} in a planned rule's template, own or inherited, is a config error. A re-plan may hand a task another skill, and so another planned rule, so all planned rules for a source set the same branch, 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 says away/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/api is refused, and a planned rule takes no wait. 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:

yaml
queue:
  - skill: plan
    where: { label: awaywork-feature }
    deliver: plan
  - skill: implement
    where: { planned: true }
    deliver: pr
  - skill: prove
    where: { planned: true }
    deliver: report
yaml
tasks:
  - { 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 done result for it, newer than your approval, is on the card, and no gate holds it: plan skips it as done: its result is on <card>, and the card reads qa 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 continue result isn't done: the task runs again.
  • Nothing can wait on it: no PR to stack on, check or merge, so a plan whose after names one isn't posted. It can wait on others, as qa waits for app.
  • A planned rule delivers draft_pr, pr or report; 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:

yaml
queue:
  - skill: prove
    where: { planned: true }
    deliver: report
    gates:
      proof: { command: sh scripts/evidence-gate.sh } # reads $AWAYWORK_OUT_DIR

One 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: plan skips it as waiting for your approval (approve, task qa's result), under Needs you, and every other task of the card as waiting for your approval of task qa's result on <card>. The card reads fanned 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 or a posts it. Anything else is feedback, and ends the hold too: the card reads qa runs again on your feedback, the task runs again with the thread in item.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/qa is refused as older than the plan, and while that plan waits your reply answers the plan.
  • A planned rule's on_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: opened plus 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.

yaml
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_out

api 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 happenedThe cardIts tasks
You approvedfanned out: api no PR yet, web no PR yetapi opens its PR, then web runs on api's branch and opens its PR, stacked on api's
api failedfanned out: api no PR yet, web no PR yetweb is skipped as api opened no PR; both run again next run
api said continuefanned out: api draft says continue, web no PR yetweb 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 apifanned out: api PR open, web no PR yetthe next run stacks web on api's open PR: after api, stacks on #41
api merged before web ranfanned out: api merged, web no PR yetweb is cut from the default branch
Both PRs are openfanned out: api PR open, web PR openboth wait for your review

Stacking details ​

  • One chain. A task's opened tasks, check ones too, lie on one chain, each after the one before, and it stacks on the last. With api after { schema: opened }, web after { api: opened, schema: opened } stacks on api, which holds schema. 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 into schema's branch, web after { api: opened } stacks on schema (after api, stacks on schema's #10), cut from the default branch only once the whole stack merged.
  • merged and opened mix: merged says when it can start, opened where. web after { schema: merged, api: opened } waits as waiting for task schema's PR to merge, then stacks on api. merged means on the default branch: if api stacks on schema and merges into its branch first, web after { api: merged } waits as waiting for task schema's PR to merge, api's merged into it.
  • A task waiting on an opened task that doesn't run (waiting for an answer, unchanged, gave up, cut by a cap) is skipped as waiting 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 setup gets none of the repo's secrets, as on any awaywork PR a rule takes up again (Safety). If setup needs one (a registry token for pnpm install, say), use merged, not opened, or every stacked task fails setup.
  • Merge the stack bottom-up. Once api's PR merges and its branch is deleted, GitHub retargets web's to the default branch; awaywork does nothing. After a squash merge web still carries api'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:

yaml
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/backend

web 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 opened tasks in the task's own repo: with ui in acme/web too, web after { api: opened, ui: opened } stacks on ui and waits for api.
  • A task in the same repo gets no shipped/: a stacked one has the files in its branch, a merged one 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:

yaml
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 opened and 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 --json has them under taskPrs.api.checks, each with its state and url.
  • On Bitbucket, checks are the build statuses on the PR's head commit, by name, via twg bb prs get --statuses: SUCCESSFUL passes, INPROGRESS is pending, FAILED and STOPPED are failing. A name carrying a build number never matches.