Runyard Docs
Concepts

Boards

Durable configured views over Factory Items β€” the software-factory surfaces that workflows keep up to date.

A board is a durable, configured view over Factory Items: lane definitions in operator language, an optional project scope, and the workflows it suggests launching from tickets. Boards make the Work surface (/app#work) a software factory: tickets are the durable asks, workflows execute them, and linked runs move tickets across the board as they progress.

Concrete board and factory configurations are durable Hub state. Create them through the board APIs or import a portable definition; Git ships the validator and workflow source templates, not the running factory configuration.

The Work surface has two views over the same board configuration:

  • Board is the operator queue: draggable tickets, lane counts, and the next human action.
  • Flow is read-only: a state-machine graph of lanes, permitted moves, guarded shortcuts, lane triggers, and automatic run-status movement. It is for inspection/debugging only; board configuration changes should happen through API/CLI/MCP or agent workflows so they leave an auditable run trail.

Shape

{
  "slug": "<board-slug>",
  "title": "<board title>",
  "description": "<operator-owned description>",
  "project": "<optional project scope>",
  "lanes": [
    { "id": "<lane-id>", "label": "<lane label>", "statuses": ["intake"] }
  ],
  "defaultWorkflows": ["<workflow-slug>"],
  "runSuccessStatus": "review",
  "isDefault": false
}
  • lanes group lifecycle statuses into operator-defined columns. Every lane needs an id, label, and at least one valid status.
  • project scopes membership: "" means all Factory Items; a project name shows only that project's tickets.
  • defaultWorkflows lists workflow slugs the board uses when a lane trigger does not name its own workflow.
  • runSuccessStatus controls successful linked-run sync. The default is review for compatibility. Factory boards that own QA, promotion, release notes, and shipped evidence can set shipped for autonomous success.
  • lane.trigger makes lane movement workflow-aware without hidden magic. mode: "suggest" only nudges the operator toward a workflow; mode: "confirm" asks before enqueueing and linking a run; mode: "auto" is a server-owned automation that launches after any successful status change into that lane, regardless of whether the move came from Web, CLI, API, or MCP. A trigger can set workflow, label, description, and default input.
  • lane.guard makes some moves impossible from the board. allowFromStatuses permits entry only from specific lifecycle statuses; denyFromStatuses blocks entry from specific lifecycle statuses; message explains the blocked drop to the operator.
  • lane.transitions is the transition policy: a per-move allow-list of who can drive the cross-lane hop (manual users, specific workflows, run origins, actor ids/roles). A lane without transitions stays unrestricted, matching today's behaviour.
  • isDefault marks the board the web app opens at /app#work.

Factory boards should use the one-approval contract: research creates proposal tickets before Ready, a PM approves direction exactly once from Telegram, and the Hub records that decision before the server moves the proposal into Ready. Ready can auto-launch an autonomous workflow from durable board config. Human attention after Ready is for new product tradeoffs, authority boundaries, destructive/external actions, missing credentials, or hard blockers. These choices are concrete configuration owned by the Hub instance.

Runs move tickets

Linked runs keep the board honest without an operator shepherding every card. The mapping is deliberately conservative:

Linked run becomesTicket moves toNotes
queued / assigned / runningrunningIncluding a blocked ticket β€” relaunching is the unblock signal (the stale blockedReason is cleared).
waiting_approval / paused / budget_exceededwaitingThe "Needs decision" lane.
succeededreview or shippedreview is the compatibility default. Boards with runSuccessStatus: "shipped" opt into autonomous success.
failedblockedWith an explicit blockedReason; there is still no failed ticket state.
cancelledβ€”Cancelling is already an operator act.

Tickets in shipped, accepted, or archived are never touched, and terminal outcomes (succeeded/failed) wait until no sibling linked run is still live. Every automated move lands in ticket history as a work_item.status_changed event attributed to the run (actor: "run:run_…"), with the reason.

Server-owned auto triggers

When a work-item status PATCH succeeds, the Hub checks every board whose project scope includes the ticket, finds the destination lane, and launches only trigger.mode: "auto" lanes. It resolves lane.trigger.workflow first, then the board's first defaultWorkflows entry, and starts the run through the same dispatcher used by HTTP schedules and manual run creation. The linked run input includes the ticket id, title, description, acceptance criteria, next action, project/type/priority, board/lane metadata, trigger metadata, and configured trigger input.

Duplicate suppression is server-side: a live linked run prevents a second launch, and a lane-entry event that already launched records an idempotent duplicate instead of starting another run. Launch evidence is written to ticket history and audit. If launch fails after the status mutation, the PATCH still succeeds; the ticket is moved to waiting with a clear blockedReason and nextAction, and the response includes laneTriggers metadata.

Before approving a proposal into Ready, callers can preview the same lane-enter plan with GET /api/factory-items/{id}/status-preview?status=ready&boardSlug=<board>. The preview does not write anything. It returns the transition policy decision, destination lane, trigger mode, resolved workflow, synthesized run input, duplicate/live-run suppression, and deterministic run preflight. Web uses this for Ready moves: green preflight shows the workflow and warnings before confirmation; needs_input can be turned into a run draft by PATCHing with negotiate: true; blocked reports the exact operator/config blockers instead of launching.

Transition policy

A transition is a cross-lane move on the board (e.g. Ready β†’ In motion). By default every move a work-item PATCH proposes is allowed β€” that's what today's Factory board does. Adding a transitions[] entry to a lane turns it into an allow-list: the move is permitted only when the caller matches one of the listed channels.

{
  "id": "review",
  "label": "Review / approve",
  "statuses": ["review"],
  "transitions": [
    {
      "to": "shipped",
      "allow": {
        "manual": true,
        "workflows": ["release-cut"],
        "runOrigins": ["schedule"],
        "actors": ["release-bot"],
        "actorRoles": ["human"]
      },
      "message": "Only humans mark shipped from Review β€” raise an approval instead of a bare PATCH."
    }
  ]
}

The allow clause is a union: any single match permits the move. Channels available today:

ChannelWhat it matches
manualA human or an authenticated token that hasn't declared a workflow/run origin (the default for any PATCH).
workflowsA workflow slug β€” set workflowSlug on the PATCH body (or the run whose status change is driving the move).
runOriginsThe originating run's origin.type (e.g. schedule, workflow, mcp, cli).
actorsA stable actor label: a token name, a run id (run:<id>), or a specific caller identity.
actorRolesA role slug: manual, human, agent, runner, schedule, workflow, system.

Enforcement runs on every board whose project scope covers the ticket. A denial from any board rejects the PATCH with 409 Conflict, the board's message, and the offending {from, to} transition. Callers driving a move on behalf of a workflow or run should declare it on the PATCH body:

{
  "status": "review",
  "workflowSlug": "implement-change-gated",
  "runOrigin": "workflow",
  "runId": "run_…"
}

The read-only Flow view at /app#work continues to render the board's static topology; when a board declares a transition policy the Flow view labels each edge with who can drive it. Editing the policy still happens through the API/CLI/MCP write path β€” the graph UI stays inspection-only.

Portable board definitions

A board definition is a portable JSON document describing a whole kanban β€” lanes, statuses, triggers, guards, transition policy, defaultWorkflows, and optional schedules[] (see Schedules). Definitions are the write path agents use to provision or reconcile boards; the DB row is one instance of a definition.

{
  "kind": "runyard.board",
  "version": 1,
  "slug": "<board-slug>",
  "title": "<board title>",
  "lanes": [ /* … lane definitions with trigger/guard/transitions … */ ],
  "schedules": [
    {
      "slug": "<schedule-slug>",
      "workflow": "<workflow-slug>",
      "cron": "<5-field cron>",
      "timezone": "<IANA timezone>",
      "enabled": false,
      "laneId": "<lane-id>",
      "input": { }
    }
  ]
}

The importer is idempotent: creating on new slugs, updating when the slug exists, and reconciling schedules[] by slug so re-imports never duplicate cron jobs. Concrete factory definitions are not shipped from Git; export an existing board with GET /api/boards/{slug}/definition or runyard board definition export <slug>, then import that document into another Hub.

YAML is deliberately deferred: the format itself is trivially expressible in YAML, but the CLI/MCP stay dependency-free, so today the documented syntax is JSON only.

API & MCP

EndpointMCP toolNotes
GET /api/boardslist_boardsAll boards, default first.
GET /api/boards/{slug}get_boardBoard + lanes with per-lane ticket counts + the decorated Factory Items in scope.
POST /api/boardscreate_boardapi/mcp scope. Body: slug, title (+ optional fields above).
PATCH /api/boards/{slug}update_boardAny subset; slug is immutable.
GET /api/board-definitionslist_board_definitionsPortable-definition summaries for boards stored on this Hub.
GET /api/board-definitions/examples/{slug}get_example_board_definitionDeprecated; concrete definitions are durable state, not Git-shipped examples.
GET /api/boards/{slug}/definitionexport_board_definitionRound-trippable JSON document for a board.
POST /api/board-definitions/validatevalidate_board_definitionSchema check without touching the DB.
POST /api/board-definitions/importimport_board_definitionCreate-or-update the board and reconcile schedule hookups.
GET /api/boards/{slug}/transitionsdescribe_board_transitionsFlatten the transition policy.
POST /api/boards/{slug}/transitions/checkcheck_board_transitionPreflight a proposed move without mutating anything.

CLI:

# The existing verbs still work as before:
runyard board list
runyard board show <slug>
runyard board create --slug team-board --title "Team Board"

# Portable definitions:
runyard board definition list
runyard board definition validate --file board.json
runyard board definition import --file board.json
runyard board definition export <slug>

# Transition policy inspection:
runyard board policy <slug>
runyard board check <slug> --from ready --to running --workflow implement-change-gated --run-origin workflow

Grouped /api/v1 aliases live under /api/v1/factory/boards… and /api/v1/factory/board-definitions….

On this page