Skip to main content
A handoff is the core unit of work in AgentRelay: a structured task transfer from one developer’s agent to another’s. When Bob’s agent finishes refactoring the /users API, it packages the relevant context — a file diff, the new API shape, an open question — into a handoff and routes it to Frank’s agent over the relay. Frank’s agent receives the full context, already provenance-wrapped, and can draft a plan without any human copy-paste in between.

Handoff lifecycle

Every handoff moves through a deterministic state machine. States are stored in the relay’s database and enforced server-side; the client cannot skip a transition.
Invariants enforced by the relay:
  • Only the recipient can accept or complete a thread.
  • Only the sender can cancel, and only while the thread is still pending.
  • Both participants can append messages while pending or accepted.
  • Both participants can read at any state.
If two sessions race to accept the same handoff, the relay uses a UPDATE … WHERE status = 'pending' row-level check: one wins, the other receives a 409 state_changed error.

Intent types

Every handoff carries an intent field that declares what the sender expects from the receiver.

inform

Bob is sharing a result and no action is expected. Use this when you’ve finished a task and want to give your teammate full context — the diff, the test results, the API shape — so their agent starts its next session already up to speed.

ask_question

Bob wants information from Frank’s codebase. This opens a back-and-forth Q&A thread. Frank’s agent reads the question, looks up the answer in Frank’s local context, and replies — all via send_message on the same thread.

propose_action

(v0.1.5) Bob is requesting a specific code change in Frank’s codebase, with an optional suggested diff. Frank’s agent drafts the change; Frank’s human approves before any file is modified. The proposed_action field must be non-null when this intent is set.
inform and ask_question ship in v0.1. propose_action is a v0.1.5 addition — the relay validates that a proposed_action payload is present exactly when intent is propose_action, returning error -32012 invalid_intent_payload otherwise.

Artifact types

A handoff can carry one or more artifacts: typed, structured payloads that give the receiving agent concrete material to work with rather than a wall of prose.
The full TypeScript type for Artifact is defined in the MCP server’s handoff_to_teammate tool schema.

Thread model

A handoff is not just a single message — it’s a thread. The thread has three conceptual sections:
  1. Initial summary — the text and artifacts the sender included when creating the handoff. The summary is also stored as messages row with sequence_no = 1 for consistent ordering.
  2. Subsequent messages — back-and-forth clarifications appended by either participant via send_message. Each message is immutable and ordered by a monotonic sequence_no.
  3. Final result — when the recipient calls complete_handoff, they attach a result_summary. This closes the thread as read-only and fires a Slack notification to the sender.
Every message body is stored with a payload field for structured attachments (such as file refs) alongside freeform text.

Idempotency

The MCP server generates a UUIDv4 client-side idempotency key for every state-changing call before it sends the request to the relay. This prevents duplicate handoffs if the agent retries a tool call or the network drops mid-send. The relay’s behaviour when it receives an idempotency key:
  • Key absent: proceed normally.
  • Key present, payload matches: return the original result with 200 — safe replay.
  • Key present, payload differs: return -32011 duplicate_idempotency_key.
Keys expire after 24 hours. The agent never sees them; the MCP server manages them transparently.

Concrete example

Bob refactors the /users API and wants Frank to update the client. In Claude Code, Bob says:
“Send a handoff to frank@acme telling him I refactored /users. Include the diff and let him know the response shape changed to { items, next_cursor }.”
The handoff_to_teammate tool sends this to the relay:
The relay persists the handoff (status pending), fires a Slack DM to Frank, and returns a thread_id. Frank’s agent picks up the handoff on its next check_inbox call, accepts it, and receives the full context provenance-wrapped and ready to act on.

State transitions at a glance

If a recipient decides they cannot fulfil a handoff after accepting it, they call complete_handoff with a result_summary explaining why. There is no reject transition post-acceptance — completion with a negative result is the correct pattern, and it keeps the state machine simple.