SecondStack · Chat App + Agent Platform · branch agent/agent-boards

Boards

Durable work items that people and Agents share. A card says what is open, who has it, and what it waits for. Chat App owns the board, the card, its history and who can see it. Agent Platform turns an assignment into a wake and runs the Agent turn. Site-defined templates add typed fields, a gated stage graph, card keys and saved views, so a board can be a team’s real tracker.

4coordination statuses
6Agent tools, fixed schemas
11template field types
≤ 3causal depth for Agent→Agent wakes
0run state stored on a card

01What Boards are, and why

Automations need a place to record work that outlives one turn: what is open, who has it, and what it waits for. A board is that place. People and Agents read and write the same cards through one validation path.

A card can be a task, a reminder to check something later, a handover from one Agent to another, or a handover from an Agent to a person. The core is small on purpose: four coordination statuses (open, waiting, done, cancelled), one assignee, an optional person the card waits on, an optional wakeAt, a Markdown body and an append-only history. There are no dependencies, claims or relations between cards.

Coordination Chat App · on the card

Intent and responsibility: title, body, status, assignee, waiting-on person, wakeAt, revision, template fields and stage, comments and history.

The card answers “who must act now, and on what?”

Execution Agent Platform · linked

Wake fires (queued, staged, running, settled), admissions, the Flue conversation and delivery. Run state is derived from the linked fires on every read and is never written to the card.

A card’s run state is its starting or running wake when one exists, else its newest wake.

A card is not a run record. An Agent keeps its per-response plan in update_plan. A card is for work that crosses turns, Agents or people. This keeps the card truthful: an Agent can never leave a card that says “running” after its process died.

Lessons from the market (research of October 2026)

  • Execution ledgers do not survive; work items that link to runs do. OpenClaw added a SQLite Tasks ledger with a task-flow layer, then removed all of it (“each runtime owns execution and completion”). Its Kanban Workboard plugin, with Agent assignees and links to sessions, survived.
  • Derive Agent state, do not let the Agent set it. Linear makes the Agent the issue’s delegate while a person stays accountable, and derives the Agent session state (active, awaitingInput, error, …) from emitted activity.
  • “Needs a human” is a state, not a product. A2A INPUT_REQUIRED, MCP input_required and the waiting-for-user states of coding agents all model it on the work item. Standalone agent-inbox products were archived or deprecated. Boards model it as waiting plus waitingOnUserId and a badge.
  • Fixed generic tools plus a schema read. Notion, monday, Airtable, Jira and OpenProject expose custom fields through generic tools, a describe-schema call and server-side validation, not per-tenant tool schemas. Boards do the same, so tool lists stay stable for prompt caching.

02What it enables

Each scenario uses the same six tools, the same card, and the same wake pipeline. Only the actor and the destination change.

Delegate to a named Agent

A person creates a card on a Project board and assigns it to a named Agent that is a member of the Project. The Agent wakes at once in a card thread inside the Project, reads the card with card_get, comments its result and finishes the card.

assign → Chat commits → wake → card thread → card_get → card_comment → done

Agent → Agent handover

Inside a woken turn, an Agent assigns a card to another Agent. The new wake gets the next causal depth. The causal person must hold invoke on the target. Above depth 3 the assignment is recorded but no wake starts (depth_exceeded), so loops stop by design.

person (depth 0) → Agent A (1) → Agent B (2) → Agent C (3) → recorded, not woken

Agent → person handover

The Agent sets status: waiting and waitingOnUserId. The person sees the attention badge. Only that person, or a board manager, can clear the wait. Their comment wakes the assigned Agent and returns the card to open.

card_update(waiting, person) → badge +1 → person comments → open + wake

“Check later”

An Agent assigns a card to itself with a future wakeAt. Self-assignment never wakes at once; the future time is a reminder. The fire sleeps durably and wakes the Agent at that time, also when the Agent is running then (a queued follow-up).

card_update(assignee: self, wakeAt: tomorrow 09:00) → durable sleep → wake

Recurring work

A Job (recurring schedule) is shown on a board as a schedule card with its timing, next run and last result. Each run can create an occurrence card with card_create, for example a weekly review assigned to a person. There is no second recurrence engine.

Slack and Telegram

The Personal Assistant in a DM, and a named Agent in a room or DM, use the same tools. An assignment made there by the same presence delivers the wake result back to that conversation through the existing external schedule destinations. “Waiting on you” is not pushed to Slack or Telegram; it is a badge in Chat.

Industry template: a CPG commercial team

The default site ships a commercial-tasks template (“Commercial team tasks”, card key TASK) that models a category-management team in consumer goods retail. It was shaped by a fit check against a customer’s own dashboard prototype; the template itself is generic.

Business conceptHow the template models it
Task typeRequired select with colour and icon per option: Losses, Revenue, Price positioning, Assortment, Suppliers, Promo, Merchandising. The board view groups cards by it inside each stage column.
Product directionselect: Fresh, Dairy, Grocery, Beverages, Household.
Weighted commercial goalsmultiselect whose options carry scalar attributes, for example Margin growth {weight: 20, priority: 1, owner: Category Director}. Agents read the attributes in board_get; the option picker shows them as one short line. Core does not compute scores from them.
Prep start, start, finishThree datetime fields. start has role start, finish has role due: a past due value on an unfinished card shows an Overdue badge.
Labor hours, TO %, FM %, TR %number fields; the three effect metrics have help text (turnover, front margin and traffic change in percent).
Budgetmoney with currency EUR, stored as an exact decimal string.
Reviewerperson field (a local Chat user; a ControlTower id is accepted and mapped).
WorkflowStages Not started → In progress → In review → Done, plus Cancelled. Entering In review requires labor_hours and finish. Done and Cancelled are terminal and can reopen.
ViewsBoard (stage columns, type sub-groups), Table (14 columns), and My tasks (filter assignee: me).

A typical flow: a category manager creates a card of type Losses for the Dairy direction and assigns it to a named analysis Agent. The Agent reads the template with board_get, writes its findings as a comment, fills labor_hours and to_pct, moves the card to In review, and sets it to wait on the reviewer. The reviewer replies on the card, which wakes the Agent again.

03System architecture

The split follows authorization. Chat App already owns the membership directory, Project membership and Agent grants for Skills, so it owns boards. Agent Platform owns only the wake and the execution it starts, and it never reads chainlit tables.

Boards system architecture People, site Git and Slack or Telegram at the top. Chat App on the left owns Board, BoardCard, BoardCardEvent and BoardTemplate, the REST API, resource access, internal routes, the tool bridge and socket pushes. Agent Platform on the right owns the management API, agent_task_fire rows with trigger kind board_card, the Absurd fire task, Flue execution and the kernel board tools. Numbered edges connect them. Integrations is not involved. People in Chat Boards rail · card dialog Site Git board-templates.yaml Slack · Telegram PA DMs · named-Agent rooms, DMs Chat App boards · cards · events · sharing · badge · templates Board BoardCard BoardCardEvent BoardTemplate REST /boards/api UI routes · one validation path resource_access grants + place-implied access internal Agent Platform routes board-wakes:check · named-agent-boards:authorize card-thread staging (shared with schedules) chat-context bridge chat-context-tools:execute turn authority · idempotency socket pushes board_attention board_changed Agent Platform wake fires · executions web · management + chat API POST · GET /v1/management/board-wakes /v1/chat/named-agent-boards:validate · :schedules agent_task_fire trigger_kind = board_card board_card_id board_executor_key Absurd fire task durable sleep to wakeAt follow-up wait · recheck shared with schedules admission + Flue turn board-wake:{fireId} payer key · delivery kernel board tools board_list · board_get card_get · card_create card_update · card_comment PostgreSQL — one schema per owner chainlit: Board* tables · agent_platform: agent_task_fire Integrations — not involved no credentials · no external effects 1 2 7 3 6 4 5 tool call with the turn's board authority
Chat App: owner of board state people and their surfaces component or route not involved
  1. People ↔ Chat: REST under /boards/api; socket events board_attention and board_changed back.
  2. Site Git → Chat: init step 22_ posts board-templates.yaml to POST /admin/board-templates/import.
  3. Chat → Agent Platform: after the card commits, POST /v1/management/board-wakes in the same request; board reads list wakes with one GET per board.
  4. Agent Platform → Chat: before staging, the fire calls board-wakes:check; then it stages the turn into the card thread. At named-Agent admission, Agent Platform asks Chat to sign a board context (named-agent-boards:authorize).
  5. Kernel tools → Chat: every board read and write goes through chat-context-tools:execute with the turn’s authority.
  6. Chat → Agent Platform: on each named-Agent tool call Chat reverse-confirms the signed context (named-agent-boards:validate) and reads the Agent’s own Jobs (:schedules).
  7. External channels: inbound turns carry board authority; a wake assigned from a channel delivers back to it.

04Data model

Four Prisma models in the chainlit schema, with logical references only (no physical foreign keys), and three columns on one Agent Platform table. Physical checks and unique indexes hold the invariants that would otherwise need code.

ModelKey columnsInvariants and notes
Board placeKind (personal · project · agent), placeId, isDefault, name (nullable), ownerUserId, access JSONB, templateId, nextCardNumber, deletedAt One live default board per place (unique partial index). Chat creates it on first use, without a stored name; the UI shows a localized name. Default boards cannot be deleted and have no template. templateId is set at creation and never changes.
BoardCard title, Markdown body, status, assigneeKind/assigneeId, waitingOnUserId, wakeAt, wakeAuthorizerUserId, revision, creator, fields JSONB, stageKey, cardNumber, kind, scheduleRef, sourceScheduleTaskId, internal metadata CHECK: status = 'waiting' exactly when waitingOnUserId is set. CHECK: a schedule card has a scheduleRef and no assignee, waiting or wakeAt. Unique (boardId, cardNumber). One live schedule card per Job taskId. Partial indexes back the attention query.
BoardCardEvent kind, actorKind/actorId, body, payload, idempotencyKey, requestHash, toolResult Append-only history and comments. Actors: user, Personal Assistant, Project Assistant, named Agent, or system for a wake that Agent Platform skipped. An Agent mutation stores its key, hash and result on its first event (unique key); a no-op stores a hidden tool_receipt.
BoardTemplate slug, version, definition JSONB, definitionHash, access, retiredAt Slug unique among live rows. The definition is the normalized YAML entry without access; its hash is SHA-256 of canonical JSON. access controls who can create boards from it, not who can see boards.
agent_task_fire
agent_platform
trigger_kind (schedule · board_card), board_card_id, board_executor_key; agent_task_id now nullable; intent in fire_payload (requestId, boardId, revision, depth, authorizer, destination) CHECK: a board fire has a card and executor and no agent_task_id, so it never appears in Jobs. Unique partial indexes per (tenant, card, executor): one queued, and one staged/running. Unique on requestId makes the POST idempotent.

Status vs stage

Status (open · waiting · done · cancelled) says who must act now. Wakes, waiting-on and attention read only status. Stage (from a template) is the business position, for example “In review”. They change independently, with one link: terminal stages set done or cancelled.

Card keys

Card creation takes the next number with UPDATE "Board" SET "nextCardNumber" = "nextCardNumber" + 1 … RETURNING inside the insert transaction, so the board row lock orders numbers. Keys read TASK-12 on a templated board and #12 elsewhere.

Migrations: apps/chat/chainlit-datalayer/prisma/migrations/20261006130000_agent_boards, 20261007120000_board_templates (which also numbers existing cards per board), and apps/agent-platform/packages/db/migrations/0048_agent_platform_board_card_fires.sql.

05Access and authority

Boards use the shared resource_access primitive with the policy read < contribute < manage. One board has one audience; there is no per-card visibility. The place a board lives in adds implied access on top of explicit grants.

Grants go to users, teams, groups and Agents. Agent grants are agents: [{id, generation, permission}], bound to the Agent ownership generation like Skill grants, so a change of ownership invalidates them. Anything granted to an Agent can appear in any turn of that Agent, the same as its Knowledge and Skills.

Place-implied access

PlaceImplied access
PersonalOwner only.
ProjectProject participants contribute, Project managers manage. Implied access ends when the board leaves the Project.
AgentAgent managers and trainers manage. The Agent itself contribute. People who only invoke the Agent get nothing from the place.

Per-presence reach

PresenceActs withReaches
Personal AssistantIts user’s live permissions, on every surface. Capped at contribute.Every board its user can read.
Named AgentOnly its own grants. Never the permissions of the person who invoked it. At most contribute.Its default board, boards of Projects where it is a member, explicit Agent grants.
Project AssistantThe Project, capped at contribute. It has no principal, so it cannot be an assignee.Only boards placed in its Project.

Rules on every surface

Assignment never shares

The assignee must already be able to read the board, or the assignment is refused. The UI then offers the share dialog. Responsibility never widens an audience.

Only the awaited person clears waiting

Only the person in waitingOnUserId, or a board manager, clears a waiting state. A comment by anyone else does not reopen the card or wake the Agent. An Agent can set a wait and comment, but cannot answer for the person.

Agents cannot create or share boards

No tool creates, shares or deletes a board, and Agents cannot link or remove schedule cards. Board structure is a human decision.

Named Agents see only their own Jobs

On a schedule card, a named Agent sees details only for Jobs it owns. It never sees the invoker’s private Jobs; the Project Assistant sees no Job details.

The signed named-Agent board context

A named Agent in a Slack room has no Chat session to borrow authority from, and it must never use the invoker’s. So Chat issues a capability that names the Agent itself.

  1. Agent PlatformAdmission asks Chat to authorize

    At admission, Agent Platform calls Chat named-agent-boards:authorize. Chat signs a named-agent-boards-v1 context bound to the Agent, its ownership generation and the admission, with an expiry.

  2. AgentEach tool call carries the context

    The kernel tool sends the signed context through the chat-context bridge. Chat checks the signature and the expiry.

  3. Chat AppReverse confirmation

    Chat confirms with Agent Platform /v1/chat/named-agent-boards:validate that the admission is live, the same pattern as Skill reads. Schedule-card details use /v1/chat/named-agent-boards:schedules with the same context.

Workbench sessions, memory dreams and unlinked channel guests get no board context. A linked channel user who never opened Chat gets a local user row at that point, so they can be an assignee or an awaited person.

06Agent tools

Six kernel tools with fixed schemas (apps/agent-platform/packages/kernel/src/tools/chat-context/board-tools.ts). There are no per-template tools: a template is discovered at run time with board_get, so the tool list stays stable for prompt caching.

ToolKindWhat it does
board_listreadBoards the turn can reach, with open and waiting card counts.
board_getreadOne board and its cards (status filter, cursor). On a templated board also the template: fields with type, required, role, currency, options and option attributes, and the stage graph. Labels carry en-US and the turn locale only.
card_getreadThe full card: body, fields, stage, allowedNextStages with blockedBy, comments, history, assignee, waiting state, wakeAt, revision, the Agent runs it started, and the schedule summary for schedule cards.
card_createwriteCreate on a board the turn can contribute to: title, body, assignee (self · user · agent), waitingOnUserId, wakeAt, fields, stageKey.
card_updatewriteChange any of the above plus status. Requires expectedRevision; a changed card refuses the update and the Agent reads it again. null unassigns or clears.
card_commentwriteAdd a Markdown comment: progress, a question or a result.

Idempotent writes

Every mutation is keyed by (admissionId, toolCallId) with a hash of the request. Chat stores the key, the hash and the result on the first event the write creates.

  • A replay with the same hash returns the stored result, also after later changes to the card.
  • A replay with a different hash is refused.
  • A no-op update stores a hidden tool_receipt event, so even “nothing changed” replays exactly.

This matters because a durable turn can retry a tool call after a crash, and a card write can wake an Agent or notify a person.

Errors the model can act on

A refused write is HTTP 400 with code: board_card_invalid, a message that names every issue, and issues: [{field | stage, problem, allowed?}]. The tool error carries it, so the next call can correct itself. A wake that does not start is named in the tool result (wakeOutcome).

Not in context

Board content is never added to the turn context. Agents pull it with the tools. Chat renders each board tool step with the card title and a link to the card.

Where the tools work

SurfaceBoard authority
Chat thread turns (also scheduled and board-woken turns)boardAccess block in the chat-context snapshot: presence, Project, causal person and depth.
Personal Assistant DMs (Slack, Telegram)The same block in the signed external personal context.
Named Agents in Chat, rooms and DMsThe signed named-agent-boards-v1 context (section 05).
Helper subagents, delegated child executionsNone: card changes wake Agents and notify people.
Workbench sessions, memory dreams, unlinked guestsNone.

07Wakes, step by step

A wake is an agent_task_fire row with trigger_kind = 'board_card'. It reuses the Absurd fire task, Chat staging, admission and delivery of schedules, but not their lifecycle, recurrence, active hours or pause-on-failure.

Board wake sequence Sequence across Person, Chat App, Agent Platform web, the Absurd fire task and the Agent turn: assign, commit, POST board-wakes, insert fire, response with wake outcome, optional durable sleep, board-wakes check, Agent checks, staging into the card thread, admission board-wake fire id, the Agent reads the card and writes back, and pushes to people. Person Chat App Agent Platformweb Fire taskAbsurd Agent turnFlue 1 PATCH card · assignee = Agent 2 commit card + event revision +1 3 POST /v1/management/board-wakes requestId · card · executor · revision · depth 4 insert fire (queued) · spawn fire id, or suppressed 5 card + wakeOutcome 6 sleep until wakeAt or wait for earlier wake 7 board-wakes:check open · same assignee · authorizer contribute · Agent reaches board ok, or skip (Chat records wake_skipped) 8 invoke · Agent state ownership generation 9 stage turn: card thread or channel 10 admission board-wake:{fireId} · trigger {kind: board_card, cardId} 11 card_get · card_comment · card_update (bridge) 12 board_changed · board_attention turn settles → fire settles
Blue arrows cross the Chat ↔ Agent Platform boundary. Dashed arrows are responses or pushes. Step 6 is skipped when the wake is due now and no earlier wake of the same card and executor is still running.

What starts a wake

  • Assigning a card to an Agent, at once, unless wakeAt is in the future.
  • A comment by the person the card waits on: the card returns to open and the assigned Agent wakes.
  • A manual wake from the card by a person; it raises the card revision.
  • Changing wakeAt on an assigned open card: a future time schedules a wake, clearing it wakes now.

Commit first, then request, in the same request. If the call to Agent Platform fails, the route returns the error with the committed card, built from Chat data only, and the card shows no linked fire, so a person can wake it again. There is no reconciler or background repair: the failure window is visible and bounded.

A wake that does not start is never silent. The card response carries wakeOutcome (requested, or suppressed with depth_exceeded or live_wake_exists); the card records wake_suppressed or wake_failed with its reason; an Agent’s tool result names the outcome.

Fire-time checks

Before staging, the fire asks Chat board-wakes:check: the card must still be open with the same assignee, the authorizing person must still hold contribute, and the Agent must still reach the board. An exact revision match is not required, so a title edit does not cancel a pending wake. Agent Platform then checks invoke, the Agent state and its ownership generation. A failed card check skips the fire (Chat records wake_skipped with the system actor); a failed Agent check fails it. Deleting an Agent cancels its live board fires.

Follow-up wakes

A card can have one queued wake in addition to one that is starting or running (staged or running) for the same executor. So a reply by the awaited person, an Agent’s own reminder and a new wakeAt during a running turn all produce a wake.

  • A request for a higher card revision cancels the queued wake as superseded.
  • A request at the same or a lower revision gets live_wake_exists.
  • A queued wake that comes due while the earlier one still runs waits durably: rechecks start at 5 s and double up to 2 min for the first 30 min, then every 10 min. After 24 h it is skipped with earlier_wake_unsettled.

Why wait instead of run in parallel? Chat does not serialize two turns in one card thread. A second turn staged before the first has answered would start a separate Agent conversation in the same thread. Two unique partial indexes make “one queued + one active” a database fact, not a code convention.

Bounds, payer and destination

Depth and self

  • Each fire carries a causal depth. An assignment to another Agent inside a woken turn gets the next depth; a reminder to self keeps its depth.
  • Depth above 3 records the assignment but starts no wake.
  • An Agent-made assignment to another Agent needs the causal person’s invoke on the target.
  • Assigning a card to yourself never wakes you at once.

Authorizer and payer

  • The authorizing person is who assigned the card, or the causal person when an Agent assigned it. A comment wake keeps the current authorizer; a manual wake uses the person who asks.
  • An invoker-funded Agent and the Personal Assistant are paid by that person’s teamless personal key. A sponsor-funded Agent pays from its own key.
  • A Personal Assistant wake runs on the person’s account default profile, which must work with an organization-scope key.

Destination, fixed at assignment

  • From Chat: a card thread with a fixed id, one per card and executor (and per person for direct named-Agent threads), inside the Project for Project boards, else a direct task thread with the authorizing person.
  • From Slack or Telegram by the same presence: back to that conversation via external schedule destinations. A named-Agent DM qualifies only with an active scheduled-DM binding; otherwise a Chat card thread.

08Templates

A template is declarative data in the site repository: typed fields, an optional stage graph with gates, a card key prefix and saved views. Core validates and renders it; nothing in a template executes code. Boards then hold stable business state as well as coordination state.

Anatomy

site-bootstrap/default/board-templates.yaml — excerpt of commercial-tasks (lines elided with # …)

boardTemplates:
  - slug: commercial-tasks
    version: "1.0"
    cardKeyPrefix: TASK
    label:
      en-US: Commercial team tasks
      ru: Задачи коммерческой команды
      pl: Zadania zespołu handlowego
      de-DE: Aufgaben des Commercial-Teams
      es: Tareas del equipo comercial
      fr-FR: Tâches de l'équipe commerciale
    # description: … (all six locales)
    access:
      all: true
    fields:
      - key: type
        type: select
        required: true
        label: {en-US: Task type, ru: Тип задачи, pl: Typ zadania, de-DE: Aufgabentyp, es: Tipo de tarea, fr-FR: Type de tâche}
        options:
          - key: losses
            color: red
            icon: trending-down
            label: {en-US: Losses, # …}
          # revenue, price_positioning, assortment, suppliers, promo, merchandising
      - key: goals
        type: multiselect
        options:
          - key: margin_growth
            color: emerald
            icon: chart-line
            attributes: {weight: 20, priority: 1, owner: Category Director}
          # loss_reduction, price_index, availability, q4_revenue, …
      - key: start
        type: datetime
        role: start
      - key: finish
        type: datetime
        role: due
      - key: labor_hours
        type: number
      - key: to_pct
        type: number
        help: {en-US: Turnover change in percent, # …}
      - key: budget
        type: money
        currency: EUR
      - key: reviewer
        type: person
    stage:
      initial: not_started
      stages:
        - key: not_started
          next: [in_progress, cancelled]
        - key: in_progress
          next: [in_review, not_started, cancelled]
        - key: in_review
          next: [done, in_progress]
          requiresFields: [labor_hours, finish]
        - key: done
          terminal: done
          next: [in_progress]
        - key: cancelled
          terminal: cancelled
          next: [not_started]
    views:
      - key: board
        kind: board
        groupBy: stage
        subGroupBy: type
        columns: [type, direction, finish, labor_hours, assignee]
      - key: table
        kind: table
        columns: [cardKey, type, direction, goals, stage, assignee, reviewer, start, finish, labor_hours, budget, to_pct, fm_pct, tr_pct]
      - key: mine
        kind: board
        groupBy: stage
        subGroupBy: type
        filter: {assignee: me}
# every field, stage and view also has a six-locale label

Identity: a stable kebab-case slug, a publisher version label, a cardKeyPrefix ([A-Z]{2,8}), a label, an optional description, and access in the site grant syntax (all, or groups and teams by !Ref). The default site also ships a simpler team-tasks template (TEAM): type, priority with a rank attribute, due date, and To do → In progress → Review → Done.

Field types

TypeValue
text · longtextString.
numberNumber.
moneyDecimal string; one ISO 4217 currency per field.
date · datetimeYYYY-MM-DD; ISO timestamp with offset. One of them can have role start, one role due (drives overdue).
booleantrue or false.
select · multiselectOption key(s). Options have a key, a label, an optional colour, an optional icon from the fixed Lucide list BOARD_TEMPLATE_ICONS, and optional scalar attributes.
personLocal Chat user id. A ControlTower id is accepted; Chat creates the local row first, as for an assignee.
urlURL string.

Field keys match [a-z][a-z0-9_]{0,39}. Any field can be required; after every write all required fields must have a value.

Stage graph

commercial-tasks stage graph Not started moves to In progress or Cancelled. In progress moves to In review, back to Not started, or to Cancelled. In review requires labor hours and finish, and moves to Done or back to In progress. Done is terminal done and can reopen to In progress. Cancelled is terminal cancelled and can reopen to Not started. Not startedinitial · not_started In progressin_progress In reviewin_review Doneterminal: done Cancelledterminal: cancelled reopen → status open reopen gate: requiresFields labor_hours, finish sets status done clears waiting sets status cancelled
Validation requires a non-terminal initial stage and at least one terminal: done stage, so every card can finish. Purple dashed edges are reopen moves.

Write rules: one validation path

The UI routes, REST and the Agent tools all call create_board_card() and update_board_card() in chainlit/boards/board_cards.py, which validate with board_templates.py.

  • A fields patch sets the keys it names; null clears a key; unknown keys and invalid values are refused.
  • A card starts in the initial stage, so that stage’s requiresFields apply at creation. A create with another stageKey is a move from the initial stage.
  • A move must follow next, and the target’s requiresFields must have values after the same write.
  • Entering a terminal stage sets its status and clears waiting. Leaving one sets open. On a staged board, done and cancelled come only from terminal stages; a direct status change is refused with their names. In a terminal stage, status and waiting-on are read-only.
  • The card dialog and the board view take these locks from one frontend helper, boardCardStatusRules(). The card form sends only the fields a person edited, so a value the form cannot show exactly (seconds of a datetime, an Agent’s value) does not change or block an unrelated save.
  • History records fields_changed (before and after of changed keys) and stage_changed (from, to).

Views

At least one view; the first is the default. A board view groups by stage (or by status without a stage) and can collapse cards into subGroupBy select groups with counts. A table view lists columns: field keys plus the built-ins assignee, status, stage, cardKey and updatedAt. A view can carry a fixed filter (stages, statuses, assignee: me). The board read returns all cards; views and ad hoc filters apply on the client. There are no per-user saved views.

Import, re-import and retirement

Init step 22_ imports board-templates.yaml through POST /admin/board-templates/import as the board-templates entity, after Skills and the membership directory sync (SITE_BOOTSTRAP_IMPORT_MODE_BOARD_TEMPLATES: replace or skip). The YAML is the whole live set: the import creates, updates and retires templates. validate_board_template_definition() rejects any error with its YAML path.

Refused (HTTP 409, lists card keys and problems)Applies at once
  • Removing a field or option that live cards use
  • Changing type or currency of a field with values (also select ↔ multiselect: a value that still parses would change its meaning)
  • A new required field without values
  • A new requiresFields key that a card already in that stage lacks
  • Stages that are missing or no longer match a card’s status
  • Retiring a template that a live board still uses
  • New fields (optional), options and stages
  • New views, label and help changes
  • Anything that leaves every live card valid

To make a breaking change, the site ships a dbmate data migration that changes the affected cards; the next import then succeeds. Values are never reinterpreted.

Known, accepted gap: a card write that runs at the same moment as an import can still use the old definition. Imports run only at deployment.

Localization

Every label, help text and description must contain all six Chat locales: en-US, ru, pl, de-DE, es, fr-FR. The UI resolves a label the same way the backend resolves the app catalog: the exact locale (ru-RU), then its language (ru), then en-US. Agents get en-US plus the turn locale only.

FDE workflow

  1. Write the template in the customer site repository

    board-templates.yaml with six-locale labels. Site Git is the only authoring path; there is no template editor in the UI.

  2. Ship the procedure as Skills, and the Agents that work the board

    Skills explain the customer’s rules (“promos above the threshold need finance review”). Named-Agent profiles get the Project membership or Agent grant that lets them reach the boards.

  3. Import at deployment

    just init-config runs step 22_; a refused import names the cards to migrate. Breaking changes ship with a dbmate data migration in the same change.

  4. Keep enforcement where it belongs

    If a workflow must change an external system with binding checks (ERP posting, budget release), build a domain MCP service and register it in mcp-tools.yaml. That service enforces the action; the board coordinates it.

09Schedule cards and occurrences

Recurrence is Jobs. A board does not get its own recurrence engine; it gets a read-only window onto a Job, and the cards the Job’s runs create.

A schedule card is a card with kind = schedule and scheduleRef = {taskId, owner: {kind: "assistant"} | {kind: "agent", agentId}}. It has no assignee, waiting, wakeAt, fields or stage, and never wakes an Agent; a database check enforces this. One live schedule card exists per Job.

  • Link: from the Jobs editor (“Show on board”) or from a board (“Add recurring card”, which opens normal schedule creation, then links). POST /boards/api/{boardId}/schedule-cards needs contribute on the board and the caller must be able to read the Job through the Agent Platform management GET as themselves. A cancelled Job cannot be linked.
  • Remove: DELETE /boards/api/cards/{cardId} removes a schedule card only; the Job does not change. Task cards are cancelled, never removed.
  • Close: cancelling the Job through the Personal Assistant or named-Agent cancel route (also the task_cancel workspace action) sets the card cancelled with a schedule_removed event. A Job that ends another way leaves the card open, showing the Job’s lifecycle.

Run state is read, never stored

A board read fetches schedule summaries from Agent Platform as the viewer, one call per distinct owner (load_board_schedule_summaries()). The card carries schedule: {name, timing, timezone, lifecycleState, nextNominalFireAt?, lastFire?} or null when details are unavailable to that viewer.

Visibility follows the execution identity (board_schedule_visible_to_actor()): a person and their Personal Assistant see Jobs the person can read; a named Agent sees only its own Jobs; the Project Assistant sees none.

Occurrence cards

A schedule fire admits as task:{taskId}:{fireId}. When card_create runs in that turn, Chat takes the task id from the validated admission id and stores it in sourceScheduleTaskId. The schedule card lists up to 20 occurrence cards, newest first, on boards the viewer can read.

On the board, schedule cards sit in a Recurring section, never in task columns, and cannot be dragged. Cards of ended Jobs stay in a folded “Ended” group and can still be removed. Agents cannot link or remove schedule cards.

10Attention badge and live updates

One query, two socket events, and no notifications outside Chat.

board_attention

The badge counts cards the user can read that wait on the user, plus open cards assigned to the user. Chat computes it with one query (backed by two partial indexes) and pushes board_attention to the user’s sessions when a card change affects them. The attention page lists the same cards.

board_changed

Every card change and comment pushes board_changed to connected people who read the board through grants, ownership or a Project. On an Agent board only the Agent’s owner is added, from one Agent Platform lookup; its other managers and trainers see the change when they next open the board. A skipped wake notifies only the people the card names.

Pushes never block work. They run after the change commits, so a push failure never fails or delays the change or the wake check. Fan-out is bounded: no per-user Agent Platform calls. There is no push to Slack or Telegram; a Slack-only person asks their Personal Assistant.

11UI tour

Screenshots of the local demo stack (demo user, default site templates, demo data). Chat App has a Boards section in the navigation rail with the attention badge, a sidebar pane of reachable boards, and board pages; Projects and Agent pages show their default board. Select an image to open it at full size.

Dairy — commercial tasks board in Board view. Sidebar lists boards and Needs my attention with count 4. The board shows a Recurring strip with TASK-13 Weekly dairy category review, view tabs Board, Table, My tasks, filter menus, and stage columns Not started, In progress and In review. Inside Not started the Losses group is expanded with TASK-9, waiting on a person, and TASK-2; Assortment and Promo groups are collapsed. In progress shows TASK-12 with a Finished run badge and TASK-1.
Board view. A commercial-tasks board: stage columns, cards collapsed into task-type sub-groups with counts, card keys (TASK-9), template fields on the card face (type, direction, labor hours, finish), the assignee, and “Waiting on”. The schedule card TASK-13 sits in the Recurring strip, not in a column. TASK-12 shows its derived run state (“Finished”) next to its coordination state (waiting on the demo user).
The same board in Table view: rows TASK-12 down to TASK-1 with columns Title, Key, Task type as coloured badges, and Product direction; more columns continue to the right with a horizontal scrollbar.
Table view. The template’s table view: one row per card, sortable columns from the view’s columns list (14 in this template; the rest scroll horizontally).
Card dialog for TASK-10 Weekly dairy KPI review in stage Done. Stage and status selectors; the status selector is disabled with a note that status and waiting-on are locked in this stage and the card reopens by moving it to In progress. Assignee, Waiting on Nobody, Wake at, and template fields: task type, product direction, commercial goals, start, finish and labor hours.
Card dialog: fields and stage. TASK-10 is in the terminal stage Done, so status and waiting-on are locked and the dialog names the reopen move (boardCardStatusRules()). Below: the generated form for the template fields.
The same card dialog scrolled down: Fields block with Edit fields, Agent runs section saying no Agent has worked on this card yet, and Activity listing created, moved Not started to In progress to In review, changed the status, and moved In review to Done, followed by a comment box.
Card dialog: history. Append-only BoardCardEvent history with stage_changed entries for every move, the Agent runs section (empty here) and the comment box.
Card dialog for TASK-4, Cottage cheese subcategory is 36 percent behind revenue plan, stage In review, status Waiting, assignee Demo User, waiting on Igor Petrov. The description says it was raised from a KPI alert. Fields include Revenue, Dairy, Q4 revenue goal, start, finish, labor hours 8 and reviewer Igor Petrov.
Waiting on a person. TASK-4 is in the business stage In review while its coordination status is waiting on the reviewer. Stage and status are independent. Only the reviewer, or a board manager, can clear this wait; the reviewer’s comment wakes the assignee if it is an Agent.
TASK-12 card dialog scrolled to Agent runs: a Wake again button and one run, Personal Assistant, Finished, with an Open thread link. Activity shows Demo User woke the Agent, the Personal Assistant moved the card to In progress and changed the status, then a long comment from the Personal Assistant reporting that board_get failed with a cursor error and that it placed the card in Waiting on the user.
A card and its Agent run. TASK-12 after a manual wake: the run row is a linked fire with its derived state and a link to the card thread; “Wake again” requests a new wake. The Personal Assistant moved the stage, then handed the card back to the person with waiting. Note: the comment reports a board_get cursor is invalid error. That is an open bug found while preparing this page (section 13), and the handover behaviour shown is the designed fallback.
Recurring job dialog for TASK-13 Weekly dairy category review with Open job and Remove from board buttons, state Active, repeats every Monday at 9:00 AM, next run in 4 days, last run none yet, a Cards from runs section with an empty state, and activity saying Demo User showed the job on this board.
Schedule card. A Job shown on a board: timing, next and last run come from Agent Platform at read time; “Cards from runs” will list occurrence cards. “Remove from board” does not touch the Job.
Needs my attention page: Waiting on me with two cards, TASK-12 on the Dairy board and TASK-3 Supplier audit for the salad line with a red Overdue badge; Assigned to me with two cards, TEAM-3 and TASK-5. The Boards rail shows a badge of 4.
Attention. “Waiting on me” plus “Assigned to me” across every board the user can read, with the rail badge (4). TASK-3 shows the Overdue badge from its due-role field.
New board dialog with Name and Place fields and the Template menu open, listing No template, Commercial team tasks and Team tasks.
Create a board from a template. The picker lists only templates whose access grant includes the user. The template is fixed for the life of the board.
The Dairy board on a 390 pixel wide phone: header with back arrow, Share and more menu, Add and Recurring card buttons, the recurring TASK-13 strip, view tabs, wrapped filter chips, and the Not started column with the Losses group expanded showing TASK-9 and TASK-2.
Mobile (390 × 844). Below the Chat App 768 px breakpoint the stage columns stack vertically with the same sub-groups, and the filters wrap. Card dialogs keep the template form inside narrow screens (e7988ddce7).

12Intentionally not built, and Phase 2

Owner decisions: Boards provide human-visible coordination and stable business state. They are not a binding enforcement system for external business actions. Customers are assumed to have no tracker, so the native board must be rich enough to be one.

Not built, by decision

  • Card dependencies, claims, leases and relations
  • An execution ledger or Agent-maintained run status on the card
  • Per-card visibility; Agents creating or sharing boards
  • UI-authored templates, workflow designer, scripts, formulas, automation rules, SLAs
  • Per-template tools; board summaries injected into context
  • Push notifications to Slack or Telegram for “waiting on you”
  • Two-way sync with external trackers; an in-process plugin runtime; a bundled third-party tracker
  • Card attachments (first release); per-user saved views
  • A background reconciler for failed wake requests

Phase 2

  • Decision records. Decision kinds (for example finance_approval) with outcomes and a decider group; an append-only decision event by an authenticated person with a snapshot of covered fields; decision gates on stages; a decision goes visibly stale when a covered field changes. Agents can read and request, never record.
  • Agent proposals with Apply. An Agent proposes field or stage changes; the card shows Apply or Reject for a person; Apply runs the normal gates.
  • Cross-board views. One template view over all boards of that template the viewer can read, for team leads and directors.
  • Workload and timeline. Sum of a number field by assignee with bands; a timeline over start and due roles.

Later, on demand

  • External tracker events. For customers who keep a tracker as system of record: an external-event trigger kind on the same fire pipeline, with an authenticated source, a scoped destination (one Agent, thread or Project), event-id deduplication and live authority checks. MCP event support needs verification first.
  • A first FDE vertical with a domain MCP service and an inline MCP App, to prove the enforcement boundary.
  • MCP App mounts in the card panel (needs viewer-based admission), card relations, attachments, plugin packaging of templates and Skills.

13Status

Branch local, not merged

branch
agent/agent-boards, no upstream
commits
9: 34ae278c56 … 8c959836b8
size
166 files, +22.6K / −0.35K lines
design
agent-platform-boards.md, agent-platform-board-templates.md

Test coverage

  • Integration API: test_boards.py, test_board_templates.py
  • Integration websocket (real assistant turns): test_board_agent_tools.py, test_named_agent_board_tools.py
  • Unit: tests/unit/chatapp/test_board_templates.py
  • Agent Platform: board-wakes unit + integration, kernel board-tools, admission fixtures
  • Frontend: presentation, template fields and views

Review

  • Design debated by two independent reviewers (Fable, Astra) before each phase.
  • Both reviewed the core and the template commits; every confirmed finding was fixed in the follow-up commits.
  • Browser QA on the demo stack drove the UI fixes in 2d23843d47 and e7988ddce7.

Found while preparing this page, fixed in 8c959836b8.

  • A Gemini-based Personal Assistant sent cursor: "" to board_get and every read failed. Board tools now treat an empty optional string as “not provided”, so it can never change data; null still clears. An invalid cursor now tells the model how to recover.
  • The commercial-tasks stage is now labelled “Stage”, so it no longer collides with the coordination status.
  • Schedule cards name the Job’s time zone and show next and last runs in the viewer’s time with its zone.
  • Card keys stay on one line; done, cancelled and waiting cards explain why they cannot be woken.

After these fixes the five board test modules pass (34 tests) on the demo stack. Shipping needs a PR into the collaboration branch, CI and Dark Review.