Get started / Your first relay
Your first relay
Start a piece of work, save a baton, pass it to a second handle, and catch it there.
Set up a workspace
A workspace holds your team's work. A handle is a name that carries work. A handle belongs to a person or to an agent.
Create a workspace, then add a handle for each agent that will carry work. Use your own name for your handle. Here it is dana.
Pick a workspace name nobody else uses
Workspace names are shared by everyone on the service. Choose one that nobody else has, such as your team's name. Use it in place of billing in every command below. If the name is taken, the tool prints these two lines:
This request cannot be used; check its fields and send it again.
invalid: the name, reference or related record is not usable; choose a new name or check the requested ids; fix: check the documented request fields and send it again
The first line is for you. The second line is for the agent that ran the command.
The commands:
handsoff workspace create billing --owner-handle dana
Workspace billing created and selected (29)
handsoff handle add build-agent --kind agent --mine
Added agent handle build-agent
handsoff handle add review-agent --kind agent --mine
Added agent handle review-agent
handsoff handle list
dana: person
build-agent: agent
review-agent: agent
The workspace is now selected. You do not need to name it again.
Start a piece of work
Every piece of work has a name, a title and a baton. The baton is a short note that tells the next carrier where the work stands. The baton explains each part and how to write a good one. Write it in a file called baton.md. It has five parts, in this order:
## True now: what is true about the work now.## Next action: the next step, written so someone else can do it as given. End it with aDone when:line that says what you will see at the end.## Traps: what will go wrong if nobody tells the next carrier.## Evidence: what you checked, and how.## References: where to look, one line for each.
A reference line looks like - [required] source: <where> -- <why> (check: <how to check it>). The kinds are work, artifact, source, secret and other. Leave out [required] when a reference is only nice to have. A secret reference says where to find a secret. It never holds the secret.
Save this as baton.md:
## True now
The v2 invoice tables exist in staging. Nothing is copied into them yet.
## Next action
Copy the 2025 invoices into the v2 tables.
Done when: The row counts match between the old and new tables.
## Traps
The old table stores amounts in cents. The new one stores whole currency units.
## Evidence
Migration 0042 ran in staging and the billing tests passed.
## References
- [required] source: migrations/0042_invoices_v2.sql -- creates the v2 tables (check: bun test billing)
Now open the work as build-agent:
handsoff open BILL-42 --title "Billing v2 migration" --as build-agent --baton baton.md
Opened BILL-42 at r1 (work 1)
Leg 1; lease expires 2026-10-04T21:33:06.365919+00:00
You hold the work now. Nobody else can catch it until you pass it or your lease runs out. r1 is the first revision of the baton. A leg is one carrier's stretch on the work. The lease is how long you hold the work. Here it is 30 minutes. Leases and carriers explains both.
If a heading is absent, the tool refuses the baton and names the line. It saves nothing. This is what it prints for a baton that holds one line:
echo "Move the billing tables." > bad.md
handsoff open BILL-42 --title "Billing v2 migration" --as build-agent --baton bad.md
This baton does not match the Markdown form; reword it to the expected shape.
invalid: line 1: expected ## True now; fix: reword it to the expected shape; use --baton <file> or handsoff save <ref> --baton /dev/stdin <<'EOF' ... EOF (save without --baton also reads standard input), with Markdown containing these headings in order: ## True now, ## Next action (with Done when: <text>), ## Traps, ## Evidence, ## References
The first line is for you. The second line is for the agent that ran the command.
Save a baton
Save a new baton each time something real changes. Print the current baton into the file, then edit it:
handsoff baton BILL-42 --as build-agent > baton.md
Change the parts that moved. For example:
## True now
The v2 invoice tables exist in staging. Invoices 1 to 1200 of 4800 are copied. The v2 table holds 1,200 rows.
## Next action
Copy invoices 1201 to 4800, then compare the row counts.
Done when: The row counts match between the old and new tables.
## Traps
The old table stores amounts in cents. The new one stores whole currency units. Divide by 100 when you copy.
## Evidence
The copy script ran for invoices 1 to 1200. It reported 1,200 rows written.
## References
- [required] source: migrations/0042_invoices_v2.sql -- creates the v2 tables (check: bun test billing)
- artifact: scripts/copy_invoices.ts -- the copy script; it takes a first and a last invoice number
Then save it:
handsoff save BILL-42 --as build-agent --baton baton.md
accepted as r2
The tool says accepted only after the service stores the baton.
Pass it
To pass the work, offer it to another handle. Then end your leg:
handsoff offer BILL-42 --as build-agent --to review-agent
Offered BILL-42 at r2 to review-agent until 2026-10-05T21:03:06.920571+00:00
handsoff end BILL-42 --as build-agent --reason clean
Ended BILL-42: clean
The work now waits for review-agent. By default the offer stays open for a day. The reason clean tells the service that you stopped on purpose, with nothing half done. The next carrier gets no warning.
Offer the work before you end. If you end without an offer, the work shows as dropped. To offer the work to any handle, write --any in place of --to review-agent. Offers, catches and drops covers deadlines and how to take an offer back.
Catch it
Switch to the next carrier. First see what waits for it. status lists the work for every handle you hold, so it shows the offer to review-agent:
handsoff status
…
Offers waiting
WAITING BILL-42: Billing v2 migration — waiting
Workspace billing (29)
Carrier build-agent; held since 2026-10-05T04:47:38.749665+00:00; lease expires 2026-10-05T05:17:40.224467+00:00
Last accepted r2 at 2026-10-05T04:47:39.976067+00:00
waiting for review-agent until 2026-10-06T04:47:40.223721+00:00
Next: catch it
…
Catch it with continue:
handsoff continue BILL-42 --as review-agent
Caught BILL-42 at r2 from build-agent via offer
Attention
Warning: References: no work reference
This machine has no pending writes.
## True now
The v2 invoice tables exist in staging. Invoices 1 to 1200 of 4800 are copied. The v2 table holds 1,200 rows.
…
You hold the work now. The tool prints the baton, and then what to do next. Work from the baton. When something real changes, edit baton.md again. For example:
## True now
Invoices 1 to 4800 are copied. The v2 table holds 4,800 rows and so does the old table.
## Next action
Check ten random invoices against the old table, amount for amount.
Done when: All ten amounts match after the divide by 100.
## Traps
The old table stores amounts in cents. The new one stores whole currency units.
## Evidence
The copy script ran for invoices 1201 to 4800. The row counts match at 4,800 on both sides.
## References
- [required] source: migrations/0042_invoices_v2.sql -- creates the v2 tables (check: bun test billing)
- artifact: scripts/copy_invoices.ts -- the copy script; it takes a first and a last invoice number
Then save it the same way:
handsoff save BILL-42 --as review-agent --baton baton.md
accepted as r3
The warning says that the baton names no related piece of work. It is only a warning, and the save went through. To clear it, add a work reference, such as - work: BILL-41 -- the migration this one follows.
If a baton was dropped
Work drops when its carrier stops without passing it. status lists dropped work, and you catch it with continue in the same way.
To see it, open a second piece of work, BILL-43. Any baton will do, so reuse baton.md. Then end the leg with a crash and never offer the work:
handsoff open BILL-43 --title "Invoice totals report" --as build-agent --baton baton.md
Opened BILL-43 at r1 (work 2)
Leg 3; lease expires 2026-10-04T21:33:07.565197+00:00
handsoff end BILL-43 --as build-agent --reason crash
Ended BILL-43: crash
Now review-agent can catch it. The tool puts the warning first:
handsoff continue BILL-43 --as review-agent
Caught BILL-43 at r1 from build-agent via drop
Attention
DROPPED
Contact with build-agent was lost: ended with reason crash at 2026-10-04T21:03:07.7337+00:00. Its last accepted save was r1 at 2026-10-04T21:03:07.566902+00:00. It may have done more than one step after that save, so every step of the next action, and any step the baton names as in flight, may already be done, not only the first. Before you do each of those steps, check its outcome through the references, even a step that looks safe to repeat: skip it if it was done, and do only the rest if it was partly done. If you cannot tell for a step, stop and block the work.
…
Dropped If you catch a dropped baton
The last carrier stopped without passing. It may be further along than its last baton says. Before each step in Next action, check what happened to it through the References. Skip a step the carrier finished. If you cannot tell, block the work and stop.
After an interruption shows this warning in full and what to do with it.
To block the work, give the reason. You keep holding it until you unblock it:
handsoff block BILL-43 --as review-agent --reason "Cannot reach the reports database to check what ran"
Blocked BILL-43; now held
handsoff unblock BILL-43 --as review-agent
Unblocked BILL-43; now held
See where it stands
status lists the work your handles hold, the offers that wait for them, and the work that dropped. It covers every handle you hold, in every workspace. history shows every step of one piece of work, in order:
handsoff history BILL-42
HELD BILL-42: Billing v2 migration — held
Carrier review-agent; held since 2026-10-04T21:03:07.259071+00:00; lease expires 2026-10-04T21:33:07.392233+00:00
Last accepted r3 at 2026-10-04T21:03:07.390944+00:00
Next: ask the carrier
History of 1; state held
Leg 1: build-agent (agent, unknown)
opened at r1; saved r1 to r2
runtime unknown and model unknown (as reported); session unknown
ended with reason clean at 2026-10-04T21:03:07.046826+00:00; handed over to review-agent
…
Leg 2: review-agent (agent, unknown)
caught the offer at r2; saved r3
runtime unknown and model unknown (as reported); session unknown
live; lease expires 2026-10-04T21:33:07.392233+00:00
…
The words in brackets after a handle are its kind and the name of the person it belongs to. The name reads unknown when that person's sign-in page sends none.
The same history is on a web page. Open https://handsoff.run/ in your browser and choose "Sign in". Use the same team sign-in page as before. The page then lists your workspaces.
Choose a workspace to see its work. Choose a piece of work to see its legs in order. For each leg, the page shows who carried the work and what they saved. It also shows the offers, notes and drops between legs, and the latest baton.
You cannot change anything on the page. It shows batons and hand-overs, and never agent transcripts. History explains each line.
Next, tell your agents how to catch work, so they can carry it as you just did.