Yoke.sh
Conversations

Branches

Soft forks within a conversation — branch timelines, fork points, and branch-scoped queries.

Branches enable soft forks within a conversation. Forking at message N creates a branches row; subsequent messages on that line set branch_id. Main timeline messages keep branch_id = null.

Visual model

main:  … → msg#10 → msg#11 → msg#12
branch B (fork at #10):  msg#10' → msg#11' → msg#12'

Both timelines share the same conversation_id. The fork point is recorded on the branch row.

Schema

branches
├── id (PK)
├── tenant_id
├── conversation_id
├── forked_from_message_id → messages.id
└── created_at

Messages on a branch:

messages.branch_id → branches.id

Main timeline: branch_id IS NULL.

Creating a branch

SDK:

const branch = await ctx.conversations.createBranch(
  conversationId,
  forkedFromMessageId,
);

HTTP:

POST /v1/tenants/:tenant_id/conversations/:conversation_id/branches
{ "forkedFromMessageId": "..." }

Emits conversation.branch.created on the stream.

Posting on a branch

await ctx.conversations.postMessage(conversationId, body, {
  branchId: branch.id,
  type: "message",
  authorParticipantId: participantId,
});

Querying

-- Main timeline only
SELECT * FROM messages
WHERE conversation_id = $1 AND branch_id IS NULL
ORDER BY created_at;

-- Specific branch
SELECT * FROM messages
WHERE conversation_id = $1 AND branch_id = $2
ORDER BY created_at;

HTTP pagination accepts branchId query param — omit for main timeline.

GET .../messages?branchId=<uuid>&limit=50

Use cases

ScenarioBranch pattern
"Try again" from message NFork, continue alternate agent response
A/B comparisonParallel branch timelines
Edit historyPrefer type: edit + relations for replacements; branches for divergent lines

Agent processors

Processors typically read the main timeline (branch_id IS NULL) unless configured for branch-aware UIs. Branch traffic still delivers via conversation.message.posted with branchId in payload.

On this page