Data model — automation surfaces
The trigger spine that turns a board event into a bot launch, the two surfaces that fire a bot on a clock, and five things a multi-replica deployment claims or leases before it acts.
Drawn from the code at commit
61a305262— main at v3.266.1, after #2391, which carries the launch rung; how this page is kept true: README.
Block A — the trigger spine
board_configis one collection holding three document shapes. The board schema (_id= the team), the event-seq allocator (seq:<team>) and the trigger cursor (trigger-cursor:<team>). None of the three carries atenant_idfield — the tenant is the_id.- A projection row names no subscription.
kind: projectionis owed to the team's bound external board, sosub_idis empty. The binding is reached by key (forge_board_bindings._idis the team id), which is why that edge is solid, not dotted. - A bot id is a slug, resolved at launch, never a foreign key. The walk is: the team's
bot_sourcesrow for(tenant, slug), then the platform row (tenantplatform:), then the baked catalog on the server's own filesystem — so a slug that resolves to nothing is a launch failure, not a dangling reference. The same holds forissue.botand forscheduled_bots.bot_id. - The board field names are the lowercased Go names. The inner documents are
native.Issueandnative.Eventverbatim with no bson tags, so the stored fields areissue.claimleaseuntil,issue.lastrunid,issue.statereasonandevent.issueid— while the REST API renders the snake_case JSON names.
Block B — the two scheduled surfaces, side by side
- Both rows carry the same policy; their clocks live separately. A schedule-kind
trigger_subscriptionsrow has no next-fire field — the residenttrigger.Schedulerpersists its calendar and pending intentions inschedule-intents.json. Cloud slots persist inschedule_launch_intentsbeforescheduled_bots.next_fire_atadvances, so a maintenance hold survives a restart. - The two surfaces do not overlap — and together they do not cover every row.
cloudschedfiresscheduled_botsrows only; the residenttrigger.Schedulerfires subscriptions of tenant""only, the local single-host scope. So a schedule- or keepalive-kindtrigger_subscriptionsrow carrying a real team id is fired by neither. - The gate persists nothing of its own.
pkg/schedgatereturns aTickRecord; that record is the tick's trace. In cloud the record lands inaudit_events(actionschedule.tick.<decision>,tenant_idthe schedule's team).
Block C — what is leased, and by whom
- Five mechanisms, five grains. A card is held by a fenced CAS claim (marker plus epoch plus a 15-minute lease); an effect row is held by a claim id with a 2-minute horizon; one team's board binding is held, for the length of a reconciliation pass, by a watermark CAS plus a 5-minute lease; a named periodic net is held by a sticky lease in
leases; and a schedule's next fire is held by a CAS onnext_fire_at(cloudsched'sClaimTick) — no lease, exactly once per slot. Only the sticky lease is an election. - There is no dispatcher-coordinator lease.
leasesholds two names in this tree,merge-gate-sweeper(the merge-gate sweep) andboard-issue-sync(the forge→board issue sync of the repo integrations). pkg/leaseis load reduction, not mutual exclusion. A holder that stalls past its TTL can overlap its successor, so the package is for idempotent reconciliation nets whose cost scales with replicas.- A lease running out does nothing by itself. The expiry is a predicate the sweeps query, never a timer.
Collections
| Collection | Package | Document struct | Tenant field (values) | Unique / TTL indexes | Twin |
|---|---|---|---|---|---|
trigger_subscriptions | pkg/trigger | Subscription | tenant_id — a team; "" is the local scope | none unique, none TTL | memory (local, manifest-seeded each start) / Mongo (cloud) |
trigger_effects | pkg/trigger (type), pkg/dispatcher/boardmongo (store) | EffectRow | tenant_id — a team | TTL done_ttl, 7 days, partial on state: "done" | file trigger-effects.json beside the native board |
board_issues | pkg/dispatcher/boardmongo | issueDoc wrapping native.Issue | tenant_id — a team | none unique, none TTL | file issues/<id>.json |
board_events | pkg/dispatcher/boardmongo | eventDoc wrapping native.Event | tenant_id — a team | none unique, none TTL | file events.jsonl |
board_config | pkg/dispatcher/boardmongo | configDoc wrapping native.Board | the _id is the tenant: <team>, seq:<team>, trigger-cursor:<team> | the _id index only | file board.json (the seq is recovered from events.jsonl; the cursor has no twin) |
scheduled_bots | pkg/cloudsched | ScheduledBot | tenant_id — a team | none unique, none TTL | none (cloud-only) — the in-process store is test-only; ~/.iterion/schedules.yaml is the host counterpart |
schedule_launch_intents | pkg/cloudsched | LaunchIntent | schedule.tenant_id — a team | unique slot _id; no TTL | schedule-intents.json beside the native board (resident trigger scheduler) |
leases | pkg/lease | none — {_id, owner, expires_at, renewed_at} | none — deployment-wide names | the _id index only | memory-only (which elects every process, so it is for one) |
forge_board_bindings | pkg/forge | BoardBinding | the _id is the team | none unique, none TTL | none (cloud-only) — the in-process store answers the OpenAPI spec generator and tests, no runtime mode |
bot_sources | pkg/botsource | BotSource | tenant_id — a team, or platform: | tenant_slug_unique | memory-only (local mode and tests); the baked catalog is the filesystem tier |
"FS: baked catalog" | pkg/botregistry | not a collection | — | not a collection | the bundles shipped in the image, read from bots/, examples/ and .botz/ under the work dir (or Bots.Paths) |
repo_integrations | pkg/forge | RepoIntegration | tenant_id — a team | tenant_conn_repo_unique | none (cloud-only) — same as the binding above: spec generator and tests only |
runs | pkg/store/mongo | store.Run | tenant_id — a team; absent on a local or resident-surface run | none unique, none TTL | file runs/<id>/run.json |
audit_events | pkg/audit | audit.Event | tenant_id — a team on the schedule.* rows drawn here, an org on auditOrg rows, empty or either on scope: platform rows (identity) | TTL audit_ttl, 400 days | none (cloud-only); the tick records go to the local JSONL |
The three board collections share one filesystem twin, the native tracker's <store-dir>/dispatcher/ directory (board.json, issues/<id>.json, events.jsonl): the same native.Issue and native.Event values, stored on both sides — under their json tags on disk, the lowercased Go field names in Mongo.
Related
- dispatcher.md — the dispatcher actor, the card state machine, claim selection, the claim lease and its watchdog; native-tracker.md — the filesystem twin in full: layout, board schema, the
iterion issueCLI, the ingest contract. - scheduling.md — the host manifest, overlap policy, keepalive, the guard command, retry precedence.
- bot-invocations.md — the manifest capability a subscription is derived from; cloud-rest-api.md — the schedule and trigger routes.
- github-board-sync.md — the projection effect and the reconciliation pass that share the outbox; agents/engine/automation-surfaces.md — the domain index for the four trigger families.
- adr/046-event-driven-runs-trigger-spine.md, adr/094-trigger-effect-outbox.md, adr/096-board-claim-lease-and-watchdog.md, adr/097-github-projects-v2-board-sync.md.
