Download all docs
boards

Sales Board

A kanban board built for B2B sales, where every card is a long-lived pursuit — a single sales case that carries its own outreach history, meetings, quotes, and revenue weighting from first prospect touch through to won or lost.

Working with it

Opening a Sales Board launches a kanban board — its dedicated working surface.

How it appears

The same element type rendered as a definition, a circle instance, and a live workspace card.

Sb
type

Sales Board

Pursuit-based sales pipeline — each card is a long-lived sales case from ICP match to won/lost

boardsatomdefinition

When to use / not

When to use

  • Running a pursuit-based B2B sales pipeline where each deal is a case that lives for weeks or months — not a one-off task — and accumulates notes, LinkedIn touches, meetings, and quotes as it moves through nine stages.
  • Turning research candidates into deals: intake prospects into an inbox column, enrich them with a research agent, then bond the responders to permanent Organization + Contact records for consent-aware outreach.
  • Reading a stage-weighted revenue forecast or a per-rep 'my work today' queue straight off the pipeline, instead of stitching one together from a spreadsheet.
  • Filtering a leads store against an ICP and opening pursuits for every match in one pass.

When not to use

  • A generic, non-sales task board (sprints, support triage, content) — use the base `board` element; sales-board's pursuit fields, stages, and outreach ops are dead weight there.
  • Hiring and candidate tracking — `recruitment-board` is the kanban specialised for that pipeline.
  • Automating outbound email or calls as a flow with wire ports — sales-board is a data element exposing an HTTP card API; orchestrate sends with an `automation` driving the `email` / `phone-number` elements.

Topology

Created from the library and placed inside an app or circle. It is a top-level building block you compose with other elements.

Properties

research_agent_slugstring
Slug of the agent dispatched by research-run. Defaults to 'helm'.
outreach_agent_idstring
UUID of the agent element that drafts outbound messages via the compose-message flow. Resolved by the designated-agent cascade: this board-level setting takes precedence, then the circle's own `outreach_agent_id`, then the first agent element in the circle. Leave unset to use circle defaults.
email_element_slugstring
Slug of the io/email element used for outreach sends.
phone_number_element_slugstring
Slug of the io/phone-number element used for dial-card.
pursuit_modeboolean
When true (default), cards are Pursuits — long-lived sales cases that start bonded to a speculative Lead and convert to bonded Organization + Contact(s) on first real response.
card_actionsarray
Declarative card-level actions — each entry surfaces as a button on the card face (rendered by portal from the `_actions` HATEOAS list on get-card) and is callable via the `run-card-action` op. Same shape + semantics as the base board element's card_actions field; surfaced here so sales-board instances can wire per-card invocations (e.g. "Call customer", "Process deal") without having to use the generic board type. See the base board's properties.yaml for the authoritative shape documentation.
columnsarray
Kanban columns for this sales board. Sales-specific extension: each column carries an optional `stage` field that maps the column to one of the 9 pursuit stages. When a card is created or moved into the column, `metadata.stage` is seeded/synced from this mapping, so the kanban position and the pursuit lifecycle stay in lock-step without the rep having to set stage manually. Default: one column per stage (9 columns), id == stage.key so reps can address them by name (`column_id: "meeting"`) right after board create — no manual add-column calls needed (BUGS-684).

Operations

  • activityGET
  • add-cardPOST
  • add-columnPOST
  • add-filePOST
  • add-linkedin-touchPOST
  • add-note-to-cardPOST
  • add-pursuit-notePOST
  • add-quotePOST
  • add-refPOST
  • apply-scaffoldPOST
  • approve-linkedin-touchPOST
  • archive-cardPOST
  • attach-outreach-promptPOST
  • attachmentsGET
  • batch_statsGET
  • board-summaryGET
  • composePOST
  • compose-message-channelsPOST
  • compose-message-draftPOST
  • compose-message-optionsPOST
  • contextGET
  • createPOST
  • deleteDELETE
  • delete-cardDELETE
  • delete-fileDELETE
  • detach-outreach-promptPOST
  • dial-cardPOST
  • disablePOST
  • enablePOST
  • export_bundleGET
  • getGET
  • get-cardGET
  • get-fileGET
  • import_bundlePOST
  • ingest-icp-matchesPOST
  • intake-prospectPOST
  • intentionGET
  • list-call-segmentsPOST
  • list-callsPOST
  • list-cardsGET
  • list-entity-activityPOST
  • list-filesGET
  • list-linkedin-touchesGET
  • list-meetingsGET
  • list-outreach-promptsPOST
  • list-pursuit-notesGET
  • list-queuePOST
  • list-quotesGET
  • log-meetingPOST
  • mark-outcomePOST
  • mass-intake-prospectsPOST
  • move-cardPOST
  • move-historyGET
  • open-pursuitPOST
  • pipeline-forecastGET
  • promotePOST
  • provisionPOST
  • readmeGET
  • readme_updatePOST
  • remove-columnDELETE
  • remove-modifierPOST
  • remove-refDELETE
  • reorder-cardPOST
  • research-prospectPOST
  • restorePOST
  • schemaGET
  • send-outreach-campaignPOST
  • send-outreach-emailPOST
  • snooze-cardPOST
  • sourceGET
  • source_branchesGET
  • source_promotePOST
  • source_repairPOST
  • source_statusGET
  • source_validatePOST
  • statsGET
  • summarize-callPOST
  • treeGET
  • unarchive-cardPOST
  • updatePATCH
  • update-cardPUT
  • update-columnPUT
  • update-prospect-metadataPOST
  • update-quote-statusPOST
  • update_metaPATCH
  • versionGET

Composition

Errors / when it fails

Duplicate column IDs detected — each column must have a unique id
Fails unless: columns.size() <= 1 || columns.all(c, columns.filter(c2, c2.id == c.id).size() == 1)
Duplicate card_action IDs detected — each action must have a unique id
Fails unless: card_actions.size() <= 1 || card_actions.all(a, card_actions.filter(a2, a2.id == a.id).size() == 1)

Validation rules

  • email_element_slug is not set — send-outreach-email and send-outreach-campaign will fail at runtime
  • phone_number_element_slug is not set — dial-card will fail at runtime
  • Board has no columns — add columns matching your nine-stage pursuit pipeline

Sales Board (sales-board)

Category: boards | Form: | Symbol: Sb

Pursuit-based sales pipeline — each card is a long-lived sales case from ICP match to won/lost

Sales pipeline board. Each card is a pursuit — a long-lived sales case that lives on the board from inbox/triage through to won/lost. Cards in the inbox column carry prospect_* metadata (prospect_company, prospect_name, prospect_role, prospect_linkedin_url, etc.) populated by research, LinkedIn import, or the manual composer; cards bonded to a permanent Organization + Contact(s) carry organization_id + contact_ids in metadata. Nine-stage kanban (skippable): prospect_pool → enriched → linkedin → email → phone → followup → meeting → quote → closed. Stage is a free field on the card — reps can jump steps for warm prospects. Outreach ops respect consent on the bonded Contact’s endpoints. ERP order history attaches to the bonded Organization by org_no/domain. Legacy flat fields (account_name, contact_email, deal_value, win_probability, expected_close_date) remain available for backward compatibility and out-of-band forecasting.

Guide

Pursuit-based sales pipeline — each card is a long-lived sales case from ICP match to won/lost

What It Does

Sales-board is a kanban board specialised for B2B sales. Each card is a pursuit — a long-lived sales case that lives on the board from inbox/triage through to won/lost. It extends the generic board element type with pursuit-specific card fields (bonds to organizations + contacts, outcome + reason), a nine-stage pipeline, and a large set of sales operations: prospect intake, ICP-match ingest, email/LinkedIn/phone outreach, meeting and quote logging, AI-assisted message composition, and a stage-weighted revenue forecast.

The pipeline is a nine-stage kanban (prospect_pool → enriched → linkedin → email → phone → followup → meeting → quote → closed). Stages are freely skippable — a warm prospect can jump straight from enrichment to quote. Cards in the inbox column carry prospect_* metadata (company, name, role, LinkedIn URL, org-number) populated by research, LinkedIn import, or the manual composer; once a prospect responds, a card is promoted to bond a permanent organizations row plus one or more contacts rows, and outreach ops then respect consent on the bonded contact’s endpoints.

A sales-board is a data element (activity_type: data), not a flow element: it exposes an HTTP card/column API rather than wire ports. It accepts rate-limit, auth-policy, and prompt modifiers, and references contacts, organizations, email, phone-number, and prompt elements in its spec and ops. Its runtime states are provisioned (initial), active, and error.

Element Definition

PropertyValue
Typesales-board
Categoryboards
Formatom
SymbolSb
Icontrending_up / #10B981
Workbenchworkbench-board-panel

Properties

FieldTypeDefaultDescription
research_agent_slugstringhelmSlug of the agent dispatched by research-prospect
outreach_agent_idstring (uuid)Agent element that drafts outbound messages via the compose-message flow. Resolved by the designated-agent cascade (board-level, then circle, then first agent). Leave unset for circle defaults
email_element_slugstringSlug of the io/email element used for outreach sends
phone_number_element_slugstringSlug of the io/phone-number element used for dial-card
pursuit_modebooleantrueWhen true, cards are pursuits — long-lived cases that convert from speculative prospect to bonded Organization + Contact(s) on first real response
card_actionsarray[]Declarative per-card action buttons; each entry (id, label, invokes) surfaces on the card face and is callable via run-card-action. Same shape as the base board’s card_actions
columnsarray9-stage defaultKanban columns. Each requires id, name, phase (todo/in_progress/closed); optional position, wip_limit, color, stage. Default seeds one column per stage, id == stage.key
stagesarray9-stage defaultThe canonical nine-stage pipeline (step, name, key, intent). Panel-hidden; defines the kanban column set

The nine default stages (step/key): 0 prospect_pool, 1 enriched, 2 linkedin, 3 email, 4 phone, 5 followup, 6 meeting, 7 quote, 8 closed. A column’s optional stage field maps it to a pursuit stage; on card create/move into that column, metadata.stage is seeded/synced from the mapping. Multiple columns may map to the same stage (e.g. Won and Lost both → closed, distinguished by outcome).

Modifiers & References

RelationshipElements
Attaches (modifiers)rate-limit, auth-policy, prompt
Uses (spec/op references)contacts, organizations, prompt, email, phone-number

Capabilities

CapabilityDescription
pursuit-trackingLong-lived sales pursuits bonded to Organization + Contact records
prospect-intakeInbox-column prospect intake before conversion to permanent records
icp-match-ingestFilter a data/leads store against an ICP modifier and open pursuits for matches
email-outreachOutreach email sending via the linked io/email element
call-outreachOutbound SIP calls via the linked io/phone-number element with live transcript capture
linkedin-touchesLinkedIn interaction logging with manual approval gate for requests and messages
meetingsMeeting scheduling and outcome logging
quotesQuote lifecycle management (draft/sent/accepted/rejected/expired/withdrawn)
pipeline-forecastStage-weighted revenue forecast across active pursuits
compose-messageAI-assisted outreach drafting via the chat-as-compose flow
entity-activityUnified chronological activity feed across all pursuits for a contact or organization
queue-viewPer-rep urgency-bucketed work queue across active pursuits
kanbanNine-stage kanban with WIP limits, move history, and card references

Error Codes

CodeClassRetryableDescription
SALES_BOARD_WIP_EXCEEDEDlimitNoCard move/create would exceed the target column’s wip_limit
SALES_BOARD_COLUMN_NOT_FOUNDvalidationNocolumn_id does not match any column on this board
SALES_BOARD_CARD_NOT_FOUNDvalidationNoCard does not exist or belongs to a different board
SALES_EMAIL_ELEMENT_NOT_FOUNDvalidationNospec.email_element_slug does not resolve to a valid io/email element
SALES_PHONE_ELEMENT_NOT_FOUNDvalidationNospec.phone_number_element_slug does not resolve to a valid io/phone-number element
SALES_CONSENT_REFUSEDvalidationNoContact has opted out — outreach send refused by the email element’s compliance gate
SALES_ICP_NOT_FOUNDvalidationNoingest-icp-matches: icp_id/icp_slug does not resolve to an ICP element
SALES_ICP_AMBIGUOUSvalidationNoingest-icp-matches: both ids omitted and circle has >1 ICP elements
SALES_QUOTE_INVALID_TRANSITIONvalidationNoupdate-quote-status: requested transition is not valid for the current state
SALES_LINKEDIN_TOUCH_NOT_FOUNDvalidationNoapprove-linkedin-touch: touch_id does not exist or is already sent
SALES_COMPOSE_NO_CHANNELSvalidationNocompose-message-channels: no actionable messaging channels for this card’s contact
SALES_PROMPT_NOT_FOUNDvalidationNocompose-message-draft: prompt_slug does not resolve to a prompt element

Operations

Operations are invoked at POST /api/{circle}/{slug}/ops/{op} (the path column below is the op’s declared route). Sales-board overrides the card-shaped ops to add pursuit fields and adds the sales-specific ops; it also inherits the shared kanban ops (move-card, reorder-card, archive-card, unarchive-card, delete-card, add-ref, remove-ref, add-column, update-column, remove-column, add-file, get-file, list-files, delete-file, move-history).

Lifecycle & provisioning

provision

POST provision · auth: write Auto-provision sibling elements for this board (reads the scaffold’s provisions list, creates siblings in the same circle, writes resolved slugs back to spec). Idempotent — skips elements that already exist.

apply-scaffold

POST apply-scaffold · auth: write Apply a named scaffold (e.g. swedish-b2b) to upgrade a blank board: writes spec._scaffold, then re-runs the provision flow. Requires scaffold.

Intake

intake-prospect

POST intake-prospect · auth: write The rep’s primary intake verb. Creates a pursuit card in the inbox column with prospect_* metadata parsed from a flexible input (LinkedIn URL, company name, or “Name @ Company”). The card carries no organization_id/contact_ids yet (speculative, no consent). When dedup_on is true (default), an existing inbox card matching prospect_org_no or prospect_email is returned instead of creating a duplicate.

mass-intake-prospects

POST mass-intake-prospects · auth: write Bulk create inbox cards from a list of prospects (items, same shape as intake-prospect). Per-item dedup is inherited; item-level errors don’t abort the batch. Returns created, deduplicated, failed, and per-item results.

update-prospect-metadata

POST update-prospect-metadata · auth: write Write research findings back onto an inbox card (card_id required). Idempotent; shallow-merges the enrichment JSONB so multiple research passes accumulate.

Card operations

add-card

POST cards · auth: write Create a pursuit card in a column (title required). Pass organization_id + contact_ids to bond permanent records; outcome (won/lost) + outcome_reason for terminal cards; plus standard card fields (description, column_id, phase, position, assignees, labels, due_date, priority, tags). When the target column declares a stage, the card is seeded with it.

update-card

PUT cards/{card_id} · auth: write Update any fields on a pursuit card (card_id required); only provided fields change. Bonds, outcome, and outcome_reason are all updatable here.

get-card

GET cards/{card_id} · auth: read Return full card detail including bonds, outcome, references, children, and file count.

list-cards

GET cards · auth: read List pursuit cards with optional filters: column_id, phase, assignee, label, archived (default false), limit (default 100), offset (default 0).

board-summary

GET summary · auth: read Pipeline overview with per-column card count, total value (newest non-withdrawn quote per card), weighted value (value × stage probability), and deal count.

Sales-specific

pipeline-forecast

GET forecast · auth: read Stage-weighted revenue forecast across all active (non-archived, non-closed) pursuits. Picks each pursuit’s newest non-withdrawn quote and multiplies its amount by a stage probability (prospect_pool=0.05, enriched=0.10, linkedin=0.15, email=0.20, phone=0.30, followup=0.35, meeting=0.50, quote=0.75). Optional from_date/to_date and metric_type (quote_value default, or quote_count). Returns total_value, weighted_value, deal_count, and per-stage breakdown.

add-pursuit-note / list-pursuit-notes

POST cards/{card_id}/notes (write) · GET cards/{card_id}/notes (read) Append-only notes on a pursuit. kind distinguishes types (note/signal/objection/commitment/followup-due, free-form). Notes are immutable; correct one by adding another. List returns newest-first.

add-linkedin-touch / approve-linkedin-touch / list-linkedin-touches

POST cards/{card_id}/linkedin-touches (write) · POST linkedin-touches/{touch_id}/approve (write) · GET cards/{card_id}/linkedin-touches (read) Log a LinkedIn touch (visit/connection_request/message). Visits auto-status sent; requests and messages default to draft and must flow through approve-linkedin-touch (flips draft → sent, stamps sent_at) before being treated as sent. This is a data-store only — it does not automate the LinkedIn interaction.

log-meeting / list-meetings

POST cards/{card_id}/meetings (write) · GET cards/{card_id}/meetings (read) Record a meeting (visit/video/phone_meeting/other). scheduled_at and occurred_at are independent (upcoming vs completed vs reconstructed). List orders newest-first by occurred_at when set.

add-quote / update-quote-status / list-quotes

POST cards/{card_id}/quotes (write) · POST quotes/{quote_id}/status (write) · GET cards/{card_id}/quotes (read) Attach a quote (defaults to status: draft; amount_cents + currency are flat forecasting fields; file_ref points to a PDF stored via add-file). update-quote-status advances through draft/sent/accepted/rejected/expired/ withdrawn, set-once stamping sent_at/responded_at.

list-entity-activity

POST list-entity-activity · auth: read Unified chronological feed across every pursuit bonded to a contact_id OR an organization_id (exactly one required). Unions notes, LinkedIn touches, meetings, and quotes into one newest-first stream.

snooze-card

POST cards/{card_id}/snooze · auth: write Snooze a pursuit for days (0–90) — hides it from the Queue until then. days = 0 clears the snooze. Card-level (not per-user) in v1.

list-queue

POST list-queue · auth: read “My work today” — buckets every active card into overdue/today/later by stage thresholds. Optional owner (user UUID) filters to that assignee; include_later (default false) and limit (default 50) control the response.

ingest-icp-matches

POST ingest-icp-matches · auth: write Filter the circle’s leads against an ICP element’s rules and call open-pursuit for every match. ICP selection: icp_id, else icp_slug, else auto-detect when exactly one ICP exists. Default stages filter ['qualified', 'enriched']. Returns candidates evaluated, matches, pursuits created, and per-lead errors.

open-pursuit

POST open-pursuit · auth: write Canonical entry for ERP-backed pursuits. Pass organization_id (required) and optional contact_ids; the pursuit starts at initial_stage (default prospect_pool; cannot be closed — close via mark-outcome).

dial-card / list-calls / list-call-segments / summarize-call

POST dial-card (write) · POST list-calls (read) · POST list-call-segments (read) · POST summarize-call (write) Place an outbound call via the phone_number_element_id’s SIP connector (card_id + phone_number_element_id required); call history lives in circle_*.calls. list-calls scopes by card_id or returns the board log; list-call-segments reads transcribed peer-side utterances for a call_id; summarize-call runs a brain turn over the transcript and appends the summary as a note.

mark-outcome

POST mark-outcome · auth: write Record a deal outcome (won/lost/no_answer/follow_up). When follow_up, optionally auto-creates a follow-up card in follow_up_column_id.

Outreach prompts (modifier sockets)

attach-outreach-prompt / detach-outreach-prompt / list-outreach-prompts

POST attach-outreach-prompt (write) · POST detach-outreach-prompt (write) · POST list-outreach-prompts (read) Bind a prompt element to the board under a slot_name (upsert by slot, optional priority) so it appears in the compose-message picker. Detach by slot_name. List resolves attached prompts to full spec.

Chat-as-compose

compose-message-channels (step 1)

POST compose-message-channels · auth: read For a card_id, returns the messaging channels actually sendable: the intersection of the contact’s verified+consented endpoints with the circle’s bonded io/* elements. Returns an empty channels array with a reason when nothing is actionable.

compose-message-options (step 2)

POST compose-message-options · auth: read For a channel, lists prompt elements in the library whose spec.channel matches (plus unset as fallback). Optional intent filter.

compose-message-draft (step 3)

POST compose-message-draft · auth: write Draft an outbound message with AI: given card_id + channel + handle and a prompt_slug (or free-form custom_prompt), resolves the contact and designated agent, renders the prompts, calls the LLM, and returns the drafted body. Idempotent per draft_id.

Outreach send

add-note-to-card

POST add-note-to-card · auth: write Append a timestamped free-text note (card_id + note required).

send-outreach-email

POST send-outreach-email · auth: write Send an outreach email via spec.email_element_slug (card_id + subject + body required; content_type text/html). The email element’s compliance gates (suppression, consent, rate limit) apply — an opted-out contact is refused with a structured error.

send-outreach-campaign

POST send-outreach-campaign · auth: write Like send-outreach-email but uses the email element’s tracked send for open/click/reply tracking; consent is strictly enforced (consent_status must be active). Optional campaign_tag.

research-prospect

POST research-prospect · auth: write Run the research agent (spec.research_agent_slug, default helm) on a single card (card_id required). The agent synthesizes news, signals, LinkedIn depth, and public records into an approach plan and writes findings back via update-prospect-metadata. Returns a generation_id for progress streaming.

Quick Start

Create a sales board

POST /api/{circle}/{project}/
Content-Type: application/json

{
  "element_type": "sales-board",
  "slug": "swedish-b2b",
  "name": "Swedish B2B Pipeline",
  "spec": {
    "email_element_slug": "outreach-mail",
    "phone_number_element_slug": "rep-line"
  }
}

The board defaults to nine columns (one per stage). Provide email_element_slug / phone_number_element_slug up front so the outreach and dial ops resolve their siblings.

Intake a prospect into the inbox

POST /api/{circle}/{project}/swedish-b2b/ops/intake-prospect
Content-Type: application/json

{
  "prospect_company": "Acme AB",
  "prospect_name": "Anna Karlsson",
  "prospect_role": "CTO",
  "prospect_org_no": "556677-8899",
  "prospect_source": "allabolag"
}

Enrich, then read the forecast

POST /api/{circle}/{project}/swedish-b2b/ops/research-prospect
Content-Type: application/json

{ "card_id": "<card-uuid>" }
GET /api/{circle}/{project}/swedish-b2b/ops/pipeline-forecast

Common Mistakes

Duplicate column IDs. Every column in spec.columns must have a unique id — duplicates corrupt card routing and fail spec validation (column_ids_unique). The same uniqueness rule applies to card_actions[].id (card_action_ids_unique).

Calling outreach ops without the sibling slugs set. send-outreach-email and send-outreach-campaign need spec.email_element_slug; dial-card needs a resolvable io/phone-number element. Unset slugs are flagged as spec warnings and the ops fail at runtime (SALES_EMAIL_ELEMENT_NOT_FOUND / SALES_PHONE_ELEMENT_NOT_FOUND).

Removing every column. A board with no columns has nowhere to put pursuits — keep columns matching your nine-stage pipeline. The empty-columns case is flagged as a spec warning.

Sending to an opted-out contact. Outreach respects the email element’s compliance gate; a contact that has opted out is refused with SALES_CONSENT_REFUSED, not silently dropped.

Closing via open-pursuit’s initial_stage. initial_stage cannot be closed — record the terminal outcome with mark-outcome (or set outcome on the card) instead.

Sending an already-sent LinkedIn message again. connection_request and message touches start as draft and must be approved once via approve-linkedin-touch. Re-approving an already-sent touch returns SALES_LINKEDIN_TOUCH_NOT_FOUND (no state change).

Relationships

  • Attaches to: rate-limit, auth-policy, prompt
  • Uses: contacts, organizations, prompt, email, phone-number

Capabilities

  • pursuit-tracking: Long-lived sales pursuits bonded to Organization + Contact records
  • prospect-intake: inbox-column prospect intake (intake-prospect, mass-intake-prospects) before conversion to permanent records
  • icp-match-ingest: Filter a data/leads store against an ICP modifier and open pursuits for matches via ingest-icp-matches
  • email-outreach: Outreach email sending via linked io/email element (send-outreach-email, send-outreach-campaign)
  • call-outreach: Outbound SIP calls via linked io/phone-number element (dial-card) with live transcript capture
  • linkedin-touches: LinkedIn interaction logging with manual approval gate for connection requests and messages
  • meetings: Meeting scheduling and outcome logging (log-meeting, list-meetings)
  • quotes: Quote lifecycle management — draft/sent/accepted/rejected/expired/withdrawn (add-quote, update-quote-status)
  • pipeline-forecast: Stage-weighted revenue forecast across active pursuits (pipeline-forecast)
  • compose-message: AI-assisted outreach drafting via chat-as-compose flow (compose-message-channels/options/draft)
  • entity-activity: Unified chronological activity feed across all pursuits for a contact or organization
  • queue-view: Per-rep urgency-bucketed work queue across active pursuits (list-queue)
  • kanban: Nine-stage kanban with WIP limits, move history, and card references

Properties

PropertyTypeDefaultDescription
research_agent_slugstringSlug of the agent dispatched by research-run. Defaults to ‘helm’.
outreach_agent_idstringUUID of the agent element that drafts outbound messages via the compose-message flow. Resolved by the designated-agent cascade: this board-level setting takes precedence, then the circle’s own outreach_agent_id, then the first agent element in the circle. Leave unset to use circle defaults.
email_element_slugstringSlug of the io/email element used for outreach sends.
phone_number_element_slugstringSlug of the io/phone-number element used for dial-card.
pursuit_modebooleantrueWhen true (default), cards are Pursuits — long-lived sales cases that start bonded to a speculative Lead and convert to bonded Organization + Contact(s) on first real response.
card_actionsarray[]Declarative card-level actions — each entry surfaces as a button on the card face (rendered by portal from the _actions HATEOAS list on get-card) and is callable via the run-card-action op. Same shape + semantics as the base board element’s card_actions field; surfaced here so sales-board instances can wire per-card invocations (e.g. “Call customer”, “Process deal”) without having to use the generic board type. See the base board’s properties.yaml for the authoritative shape documentation.
columnsarray[{"id":"prospect_pool","name":"Prospect pool","phase":"todo","position":0,"stage":"prospect_pool"},{"id":"enriched","name":"Enriched","phase":"in_progress","position":1,"stage":"enriched"},{"id":"linkedin","name":"LinkedIn warm-up","phase":"in_progress","position":2,"stage":"linkedin"},{"id":"email","name":"Email outreach","phase":"in_progress","position":3,"stage":"email"},{"id":"phone","name":"Phone","phase":"in_progress","position":4,"stage":"phone"},{"id":"followup","name":"Follow-up","phase":"in_progress","position":5,"stage":"followup"},{"id":"meeting","name":"Meeting","phase":"in_progress","position":6,"stage":"meeting"},{"id":"quote","name":"Quote","phase":"in_progress","position":7,"stage":"quote"},{"id":"closed","name":"Closed","phase":"closed","position":8,"stage":"closed"}]Kanban columns for this sales board. Sales-specific extension: each column carries an optional stage field that maps the column to one of the 9 pursuit stages. When a card is created or moved into the column, metadata.stage is seeded/synced from this mapping, so the kanban position and the pursuit lifecycle stay in lock-step without the rep having to set stage manually.
Default: one column per stage (9 columns), id == stage.key so reps can address them by name (column_id: "meeting") right after board create — no manual add-column calls needed (BUGS-684).
stagesarray[{"intent":"ICP-matched companies in light-scan","key":"prospect_pool","name":"Prospect pool","step":0},{"intent":"Detailed person/company info attached; manual fields welcome","key":"enriched","name":"Enriched","step":1},{"intent":"Profile visit + connection request + message (manual approval)","key":"linkedin","name":"LinkedIn warm-up","step":2},{"intent":"Editable email with optional product catalog; manual control","key":"email","name":"Email outreach","step":3},{"intent":"Call placed, recorded, transcribed, action points extracted","key":"phone","name":"Phone","step":4},{"intent":"Thank-you summary; action items; manually approved outbound","key":"followup","name":"Follow-up","step":5},{"intent":"Physical or video meeting with prospect","key":"meeting","name":"Meeting","step":6},{"intent":"Offert sent; attached to card; awaiting response","key":"quote","name":"Quote","step":7},{"intent":"Won or Lost — outcome captured with reason","key":"closed","name":"Closed","step":8}]The nine-stage kanban pipeline (Säljstöd). Stages are freely skippable — a card may jump from Step 1 (enrichment) directly to Step 7 (quote) when a prospect is already warm. Stage progression is not enforced by the board; reps set the stage field on cards directly. The stage array here defines the canonical kanban column set.

Operations

activity

Get /ops/activity | Auth: Read

Get activity events for this element

Scope depends on element capabilities: individual elements query by element_id, project-form elements with activity-scope-members include member activities, circle-level elements with activity-scope-all query the entire circle. Gracefully returns empty list if activities table is missing (old circles).

add-card

Post /ops/cards | Auth: Write

Create a new pursuit card in a column

Creates a pursuit card in the specified column. Position defaults to the bottom of the column. Phase defaults to “todo”. Column can be specified by ID, name, or phase alias. Pursuit bonds: pass organization_id + contact_ids to root the pursuit in permanent records. Bonds are stored in card metadata and drive consent-aware outreach. Cards in the inbox column may carry prospect_* metadata (prospect_company / prospect_name / prospect_role / prospect_linkedin_url etc.) for pre-conversion pursuits sourced from research or LinkedIn import. Stage: one of prospect_pool, enriched, linkedin, email, phone, followup, meeting, quote, closed. Free-field — not enforced by column order. Skips allowed per customer requirement. When the target column declares a stage mapping, the card is seeded with that stage automatically if the caller doesn’t provide one.

add-column

Post /ops/columns | Auth: Write

Add a new column to the board

Creates a column with id, name, and phase (required). The display label field is ‘name’ (not ‘title’). Phase accepts friendly names: Backlog/To Do→todo, Sprint/In Progress→in_progress, Done/Review→closed. Use position gaps (e.g., 15 between 10 and 20) to insert between existing columns without renumbering.

add-file

Post /ops/cards/{card_id}/files | Auth: Write

Attach a file to a card

Uploads a file to the card’s CAS folder at files/{card_id}/{filename}. Content is base64-encoded in the request body. Existing files with the same name are overwritten (upsert). Creates a DB index entry for fast listing. Maximum filename length is 255 characters; path separators are rejected.

add-linkedin-touch

Post /ops/cards/{card_id}/linkedin-touches | Auth: Write

Log a LinkedIn touch against a pursuit (visit | connection_request | message)

Writes to the per-circle pursuit_linkedin_touches child table (lazily bootstrapped). Captures Säljstöd Step 2 activity:

• kind=visit — profile visit, auto-status=‘sent’ (no approval gate, as visits are passive). • kind=connection_request — defaults to status=‘draft’ so the rep can review before the connection actually goes out. • kind=message — requires a body, defaults to status=‘draft’. Per customer requirement “meddelande ska godkännas manuellt”, messages must flow through approve-linkedin-touch before they’re treated as sent.

This op is a data-store only — it does NOT automate the LinkedIn interaction. Outbound LinkedIn automation is a separate io/ connector track. Sales reps log what they did here; the board surfaces the history per pursuit.

add-note-to-card

Post /ops/add-note-to-card | Auth: Write

Append a free-text note to a card

Appends a timestamped note to the card’s notes log. Useful for post-call observations, follow-up reminders, or context that doesn’t fit in the card title.

add-pursuit-note

Post /ops/cards/{card_id}/notes | Auth: Write

Append an append-only note to a pursuit

Writes to the per-circle pursuit_notes child table (lazily bootstrapped on first use — no migration needed). Notes are immutable; to correct one, add a new note. Survives the lead → contact conversion seam: notes stay bonded to the pursuit card regardless of which permanent records the pursuit is bonded to over time. Use kind to distinguish note types. Default ‘note’ is a free rep observation. Other suggested kinds: ‘signal’ (buying signal picked up), ‘objection’, ‘commitment’, ‘followup-due’. These are free-form — filtering happens in the reader.

add-quote

Post /ops/cards/{card_id}/quotes | Auth: Write

Attach a quote (offert) to a pursuit

Writes to per-circle pursuit_quotes (lazy CREATE TABLE). Step 7 in Säljstöd — the formal proposal. Defaults to status=‘draft’; call update-quote-status to advance through sent → accepted|rejected|expired. file_ref is an opaque pointer to the PDF stored via the board’s existing add-file pipeline — store whatever resolver key add-file returns, then the UI can download via get-file. amount_cents + currency are flat fields for forecasting. valid_until is a date (not datetime) — the quote expires at end-of-day in the rep’s timezone, not a specific clock instant.

add-ref

Post /ops/cards/{card_id}/refs | Auth: Write

Add a typed reference between cards

Creates a typed edge from this card to the target. relation=blocks automatically creates the inverse blocked_by on the target card. relation=parent makes this card a child of the target. Validates against circular references (BOARD_CIRCULAR_REF) and self-references (BOARD_SELF_REF).

apply-scaffold

Post /ops/apply-scaffold | Auth: Write

Apply a named scaffold to this board (retroactive provisioning)

Like provision but with an explicit scaffold template name. Writes spec._scaffold to the given name, then re-runs the provision flow to materialize that scaffold’s sibling elements. Use this when you created a blank sales-board and want to upgrade it to a preset (e.g., swedish-b2b), or when swapping between regional scaffolds.

approve-linkedin-touch

Post /ops/linkedin-touches/{touch_id}/approve | Auth: Write

Approve a draft LinkedIn touch — flips status draft → sent

Manual-approval step for LinkedIn connection requests and messages. Records the approver UUID and timestamp, transitions status ‘draft’ → ‘sent’, stamps sent_at=now. Idempotent by effect: if the touch is already sent, the update returns NotFound (no state change). Optional evidence merges into the row’s evidence blob — use this to capture what actually went out (LinkedIn activity URL, message ID from the automation tool, screenshot of the outgoing message).

archive-card

Post /ops/cards/{card_id}/archive | Auth: Write

Archive a card (soft-remove from board view)

Archived cards are hidden from default list_cards results but can be queried with filter=archived. Archive rather than delete to preserve history and references. Archived cards do not block other cards.

attach-outreach-prompt

Post /ops/attach-outreach-prompt | Auth: Write

Attach a prompt element to this board as an outreach option (a ‘socket’)

Creates an element_modifiers row binding the prompt element to this board with modifier_kind=“prompt” and a user-chosen slot_name. The slot_name is what the user will see in the compose-message option picker (e.g. “warm_intro”, “follow_up”). Multiple prompts can be attached under different slot_names; the same slot_name replaces the existing attachment (upsert semantics).

attachments

Get /ops/attachments | Auth: Read

List all modifiers and resources attached to this element

Returns both modifiers (policy enforcement) and resources (data injection) with is_modifier flag to distinguish. Items in the generated MODIFIER_TYPES list are modifiers; everything else is a resource. Includes cascade_policy and version pin info.

batch_stats

Get /ops/batch_stats | Auth: Read

Get per-element statistics for all children of this element

Returns per-child stats plus an aggregate. Most meaningful on compound or manifest form elements (repositories, circles, projects); atoms have no children so the result is an empty children array with a zeroed aggregate. Uses efficient GROUP BY SQL. Weighted averages for eval scores.

board-summary

Get /ops/summary | Auth: Read

Pipeline overview with deal totals per stage

Returns a board overview with per-column counts and quote-derived aggregates. Each column includes: card count, total value (sum of the newest non-withdrawn quote per card), weighted value (value × stage probability), and deal count. Useful for pipeline health dashboards. Phase 5 rework: see pipeline-forecast for the weighting table.

compose

Post /ops/compose | Auth: Execute

Batch add and remove modifiers on this element in a single call

Declarative composition: add modifiers by ref path (slug or path@version) and remove by attachment ID, all in one atomic call on the target element. Each ‘add’ entry resolves the source element, validates topology, attaches with optional priority and cascade policy. Each ‘remove’ entry deletes the attachment row. Returns a summary of what was added and removed. Example: compose({ add: [{ref: “my-prompt”}, {ref: “rate-limit/api@v2”, priority: 50}], remove: [{attachment_id: “uuid”}] })

compose-message-channels

Post /ops/compose-message-channels | Auth: Read

List actionable messaging channels for a card’s primary contact (chat-as-compose step 1)

Given a card_id (and optionally an explicit contact_id), returns the messaging channels we can actually send on: the intersection of the contact’s verified+consented endpoints (emails / phones / socials / messaging from data/contacts) with the circle’s bonded io/* elements (email, slack, discord, matrix, rocketchat, mattermost, phone-number). Feeds the channel picker Question widget in the compose-message flow. Returns an empty channels array with a reason field when nothing is actionable — the portal renders that as an empty-state CTA.

compose-message-draft

Post /ops/compose-message-draft | Auth: Write

Draft an outbound message with AI (chat-as-compose step 3)

Third and final step. Given a card_id + channel + handle + prompt_slug (or free-form custom_prompt), resolves the card’s contact and the designated agent, renders the system prompt (contact-context-system) and user prompt (the selected template), calls the LLM, and returns the drafted body. The portal renders the result in a MessageDraft chat widget that the user can edit and send. Idempotent per draft_id. Use compose-message-options first to discover available prompt_slugs.

compose-message-options

Post /ops/compose-message-options | Auth: Read

List prompt options for a channel (chat-as-compose step 2)

Second step of the chat-as-compose flow. Given a channel slug, returns all prompt elements in the circle’s library whose spec.channel matches (or whose channel is unset, as fallback). Each option carries its slug, display_name, intention, and intent tag — the portal renders these as buttons in a Question widget. A “Custom…” option is always included client-side to let the user free-text their intent; no server-side representation needed for that.

context

Get /ops/context | Auth: Read

Get connected elements (graph traversal)

Graph traversal showing all connected elements with their relationship type (contains, contained_by, references, referenced_by, attaches, etc.). Use ?depth=N to control traversal depth (default 1) and ?types=actor,data to filter by element types.

create

Post /ops/create | Auth: Write

Create child element

POST to the parent path — element_type goes in the request body, NOT the URL. Both element_type and slug are required and must be non-empty. Name is derived from slug if omitted. Writes to both Git and PostgreSQL. All elements are stored flat under the circle — no intermediate library wrapper rows.

delete

Delete /ops/delete | Auth: Admin

Delete element (soft delete)

Soft delete — sets state to ‘deleted’ but retains the record. Cannot delete elements that have children (has_no_bond precondition) or active runs. Requires admin auth and confirmation.

delete-card

Delete /ops/cards/{card_id} | Auth: Write

Permanently delete a card

Hard deletes a card and all its references (blocked_by, blocks, related, parent). Subcards (children) are NOT deleted — they become orphans. Prefer archive-card to preserve history.

delete-file

Delete /ops/cards/{card_id}/files/{filename} | Auth: Write

Remove a file attachment from a card

Deletes the file from CAS and removes its DB index entry. The filename is extracted from the URL path. Returns not-found if the file doesn’t exist.

detach-outreach-prompt

Post /ops/detach-outreach-prompt | Auth: Write

Remove a prompt attachment from this board

Deletes the element_modifiers row for (board_id, kind=“prompt”, slot_name).

dial-card

Post /ops/dial-card | Auth: Write

Place an outbound call to the card’s contact phone

Resolves card.metadata.contact_phone (populated by add-card-from-lead) and dials via the specified phone-number element’s SipConnector. Records every attempt in circle_*.calls (card_id, lead_id, sip_call_id, status, started_at) so call history lives with the card. Returns immediately after INVITE — rollout of live RTP audio tap and post- call transcription pipeline comes in a follow-up round.

disable

Post /ops/disable | Auth: Admin

Disable element (hides and prevents use)

Idempotent — safe to call on already-disabled elements. Optionally pass a reason string. Disabled elements cannot be invoked or executed. Inverse of enable.

enable

Post /ops/enable | Auth: Admin

Enable element (makes usable and visible)

Idempotent — safe to call on already-enabled elements. Transitions element to ready/enabled state. Cannot enable deleted elements. Inverse of disable.

export_bundle

Get /ops/export/bundle | Auth: Read

Export element as downloadable git bundle

On non-root-namespace elements, returns a binary git bundle. On root-namespace (circle) elements, dispatch hands off to the circle’s own export_bundle op, which returns a multi-element JSON envelope with one base64 bundle per child element — this is intentional, not an error.

get

Get /ops/get | Auth: Read

Get element details

Element is already resolved by the routing layer — this returns the cached element, not a fresh DB query. Use the path /api/{circle}/{slug} to address elements.

get-card

Get /ops/cards/{card_id} | Auth: Read

Get a pursuit card with full details

Returns full card details including pursuit bonds (organization_id, contact_ids), stage, outcome, references, children, and file count.

get-file

Get /ops/cards/{card_id}/files/{filename} | Auth: Read

Download a file attached to a card

Returns the file content as base64 along with metadata (size, content type, upload timestamp). The filename is extracted from the URL path.

import_bundle

Post /ops/import/bundle | Auth: Write

Import git bundle into element

Accepts a base64-encoded git bundle in the JSON bundle_base64 field. Use overwrite=true to replace existing elements with same slug (default skips duplicates). Imported elements get new UUIDs. Returns counts of imported/skipped elements and any errors.

ingest-icp-matches

Post /ops/ingest-icp-matches | Auth: Write

Filter leads against an ICP modifier and open pursuits for matches

Closes the research → pursuit loop. Reads an ICP element’s rule set (industries, regions, employee/revenue bands, required signals, target roles, excluded keywords, min_score), walks the circle’s leads at specified stages, evaluates each against the rules, and calls open-pursuit for every match. ICP selection priority:

  1. icp_id (UUID) — explicit element selection.
  2. icp_slug — lookup by slug (must be an element of type icp).
  3. Auto-detect — if exactly one ICP exists in the circle, use it. Fails with a helpful error if 0 or >1 ICPs exist.

Idempotency: matched leads are moved to stage=‘assigned’ after a successful open-pursuit call. The next run won’t re-ingest them (default stages filter is {‘qualified’, ‘enriched’}). Match semantics follow the ICP’s match_mode: ‘all’ (AND) or ‘any’ (OR). Excluded keywords are always a short-circuit fail. Returns a summary: candidates evaluated, matches found, pursuits created, and a list of per-lead errors (if any open-pursuit calls failed individually, the others still proceed).

intake-prospect

Post /ops/intake-prospect | Auth: Write

Create a card in the inbox column with prospect data — the manual intake path

The rep’s primary intake verb. Accepts a flexible input shape — paste a LinkedIn URL, type a company name, or “Name @ Company” — and creates a pursuit card in the board’s inbox column (the leftmost column by position, or whichever column matches spec.intake_column_id when set). The card’s metadata is seeded with prospect_* fields parsed from the input. The card has no organization_id / contact_ids yet (it is speculative, with no consent on any contact info). When the prospect first responds, convert-on-response promotes the card by upserting an Organization + Contact with dedup, capturing soft_optin consent, and patching the card’s metadata to clear the prospect_* raw fields and fill organization_id + contact_ids. Idempotency: if prospect_org_no or prospect_email matches an existing inbox card on this board, returns that card_id rather than creating a duplicate (when dedup_on is unset or true).

intention

Get /ops/intention | Auth: Read

Get element intention with full inheritance chain

Returns three levels: direct (this element’s intention), inherited (from category and root), and resolved (final merged intention). Useful for understanding an element’s purpose in context of its hierarchy.

list-call-segments

Post /ops/list-call-segments | Auth: Read

Transcribed utterances for a call — live transcript view

Reads circle_*.call_segments populated by the sales_transcribe_loop. Each row is one VAD-delimited utterance from the prospect side of the call with text + voice_ms + started_at + ended_at. Poll this while a call is in progress to drive a live-transcript UI, and read it once post-call for the final version. Note that segments currently only contain the PEER side — capturing the rep’s own audio needs a browser-side WebRTC tap (separate feature).

list-calls

Post /ops/list-calls | Auth: Read

Call history for a card or the whole board

Filter by card_id to scope. Without it, returns the board’s full recent call log newest-first. Useful for the live-card UI and for agent-side coaching (“how did the last call with this prospect go”).

list-cards

Get /ops/cards | Auth: Read

List pursuit cards with optional filtering

Lists pursuit cards with optional filters. Returns pursuit fields (stage, bonds, outcome) alongside standard card data. Filter by column, phase, assignee, label, or archived status.

list-entity-activity

Post /ops/list-entity-activity | Auth: Read

Unified activity feed across every pursuit bonded to a contact or organization

Backs the Contact / Organization variants of the Entity Detail Surface (spec: .triform/design/board-redesign/06-entity-detail-surface.md). Given a contact_id OR organization_id, finds every card on this sales-board whose metadata references it, then unions all four per-pursuit activity tables (pursuit_notes, pursuit_linkedin_touches, pursuit_meetings, pursuit_quotes) and returns a single chronological stream, newest-first. Exactly one of contact_id / organization_id must be set. Passing both is an error (ambiguous scope); passing neither returns an empty stream rather than listing everything (guardrail against accidental full-scan calls from the UI). Row shape is normalised to match ActivityTimeline’s ActivityEntry — every bucket emits {kind, when, title, body?, status?, card_id, card_title} so the frontend doesn’t need per-kind branching.

list-files

Get /ops/cards/{card_id}/files | Auth: Read

List files attached to a card

Returns metadata for all files attached to the card. Reads from the DB index table for fast listing without walking the CAS tree.

list-linkedin-touches

Get /ops/cards/{card_id}/linkedin-touches | Auth: Read

List LinkedIn touches for a pursuit, newest first

list-meetings

Get /ops/cards/{card_id}/meetings | Auth: Read

List meetings for a pursuit (newest first, by occurred_at if set)

list-outreach-prompts

Post /ops/list-outreach-prompts | Auth: Read

List all prompt elements attached to this board

Returns the prompts currently attached to this board as outreach options, resolved to full prompt spec so the portal can show the slot_name, display_name, intention, channel tag, and intent tag.

list-pursuit-notes

Get /ops/cards/{card_id}/notes | Auth: Read

List notes on a pursuit, newest first

list-queue

Post /ops/list-queue | Auth: Read

My work today — per-user urgency-bucketed cross-pursuit feed

Backs the Queue view (spec: .triform/design/board-redesign/07-queue-view-ux.md). Scans every active (non-archived, no outcome) card on this sales-board and buckets it as Overdue / Today / Later based on stage + stage_days thresholds. Same thresholds derive_next_action uses for the per-card next-action so Queue and card face stay in agreement about what’s urgent. v1 is board-scoped (one sales-board at a time). Cross-board aggregation across multiple sales-boards in a circle is a follow-up — most circles currently have one sales-board and the complexity budget for v1 is better spent on the UI primitives. Owner filtering: pass owner as a user UUID to include only cards where that user is in the card’s assignees array. Omit or pass empty to include all assignees (team queue view).

list-quotes

Get /ops/cards/{card_id}/quotes | Auth: Read

List quotes for a pursuit, newest first

log-meeting

Post /ops/cards/{card_id}/meetings | Auth: Write

Record a meeting (physical visit, video, phone) on a pursuit

Writes to per-circle pursuit_meetings (lazy CREATE TABLE). Step 6 in Säljstöd — the real-world contact that usually precedes a quote. Scheduled vs occurred timestamps are independent: scheduled_at without occurred_at = upcoming; both set = completed; only occurred_at set = reconstructed from memory. Summary is free-text (agenda, outcomes, action points). Attendees is a free JSONB shape — keep it simple, the board reads it back as-is.

mark-outcome

Post /ops/mark-outcome | Auth: Write

Record a deal outcome on a card

Sets the outcome on a card (won, lost, no_answer, follow_up). When outcome is “follow_up”, optionally auto-creates a new card in the specified column for the next attempt.

mass-intake-prospects

Post /ops/mass-intake-prospects | Auth: Write

Bulk create inbox cards from a list of prospects

Used by: • the inline composer’s multi-line paste (one prospect per line), • io/* connectors (LinkedIn ingest, Allabolag bulk lookup), • intake-prospects-from-icp’s research agent write-back, • CSV importers.

Each item has the same shape as intake-prospect’s input. Idempotency and dedup behaviour are inherited from intake-prospect — duplicates against the inbox return the existing card_id without creating new cards. Errors on individual items don’t abort the batch; the response enumerates per-item outcomes.

move-card

Post /ops/cards/{card_id}/move | Auth: Write

Move a card to a different column

Moves the card to the target column. Column can be specified by ID, name, or phase alias (Backlog, In Progress, Done, etc.). Position defaults to bottom of the target column. This is tracked in the move history audit trail. Enforces WIP limits on the target column — fails with BOARD_WIP_EXCEEDED if the limit would be breached. Emits board.card.moved NATS event.

move-history

Get /ops/cards/{card_id}/moves | Auth: Read

Get the move history for a card

Returns the audit trail of column transitions for this card. Each entry includes from_column, to_column, moved_by (actor slug), and timestamp. Ordered newest first.

open-pursuit

Post /ops/open-pursuit | Auth: Write

Open a pursuit — a sales card bonded to an existing organization

The canonical entry point for ERP-backed pursuits. Pass organization_id (and optional contact_ids). The pursuit starts at an explicit initial_stage bonded to the permanent records. For pursuits starting from research candidates, use intake-prospect (which creates a card in the inbox column with prospect_* metadata) + research-prospect (which dispatches the research agent against an existing card). The legacy pre-conversion lead-bond flow is gone.

pipeline-forecast

Get /ops/forecast | Auth: Read

Stage-weighted pipeline revenue forecast

Returns a stage-weighted revenue forecast across all active (non-archived, non-closed) pursuits. For each pursuit, picks the newest non-withdrawn quote and multiplies its amount by a stage-based probability (prospect_pool=0.05, enriched=0.10, linkedin=0.15, email=0.20, phone=0.30, followup=0.35, meeting=0.50, quote=0.75). Groups by column/stage and returns: - total_value: sum of quote amounts per stage - weighted_value: sum of (amount × stage weight) - deal_count: number of pursuits per stage - avg_stage_weight: average weight applied Pursuits without a quote contribute 0 to totals. Phase 5 implementation.

promote

Post /ops/promote | Auth: Admin

Promote element configuration to a target environment

Only for manifest-form elements (projects). Environments advance: dev → demo → live. dev→demo requires member+ role, demo→live requires admin. Freezes member versions at promotion time (creates snapshot). Persists environment config to spec.environments.

provision

Post /ops/provision | Auth: Write

Auto-provision sibling elements for this sales board

Called automatically after board creation. Reads the scaffold’s provisions list and creates sibling elements (data/leads, io/http connectors, variable secrets) in the same circle. Writes resolved slugs back to the board’s spec. Idempotent — skips elements that already exist.

readme

Get /ops/readme | Auth: Read

Get element README.md content

Reads README.md from the element’s git repository. Returns empty content (not an error) if no README exists. Always returns markdown format.

readme_update

Post /ops/readme_update | Auth: Write

Update element README.md content

Creates or overwrites README.md in the element’s git repo. Commits to the draft branch. Content must be provided as a markdown string.

remove-column

Delete /ops/columns/{column_id} | Auth: Admin

Remove a column from the board

Fails with BOARD_COLUMN_NOT_EMPTY if the column has non-archived cards. Archive or move all cards first. The column’s position gap remains available for reuse.

remove-modifier

Post /ops/remove-modifier | Auth: Execute

Remove an attached modifier from this element by attachment ID

Removes a modifier/resource attachment by its row ID. The ID comes from the attachments or context API. This is the reverse of attach — called on the target element, not the source.

remove-ref

Delete /ops/cards/{card_id}/refs/{target_card_id} | Auth: Write

Remove a reference between cards

Removes the reference from this card to the target. If the reference has an inverse (blocks/blocked_by), the inverse is also removed.

reorder-card

Post /ops/cards/{card_id}/reorder | Auth: Write

Change a card’s position within its current column

Changes vertical position (priority) without changing column. Position 0 is the top (highest priority). Other cards in the column are renumbered automatically to maintain order.

research-prospect

Post /ops/research-prospect | Auth: Write

Run the research agent on a single card for deep multi-source enrichment

The post-collapse replacement for research-run. Takes a card_id directly instead of a lead_id, so it works on the new flow where cards in the inbox column ARE the prospects (no separate data/leads element). The research agent reads the card’s prospect_* metadata fields (prospect_company / prospect_name / prospect_role / prospect_org_no / prospect_linkedin_url / etc.), synthesizes news + signals + LinkedIn depth + public records into an approach plan, and writes findings back via a single update-prospect-metadata { card_id, prospect_fields, enrichment, merge_enrichment: true } call. Re-running refreshes without clobbering thanks to the shallow-merge enrichment semantics. Resolves spec.research_agent_slug (defaults to ‘helm’) and dispatches the agent’s generate op. Returns a generation_id the frontend subscribes to via NATS for progress streaming. Coexists with research-run during the data/leads → cards migration; once leads are gone, research-run is dropped.

restore

Post /ops/restore | Auth: Admin

Restore element to a specific version

Automatically snapshots the current state before restoring (creates a ‘Before restore to vN’ version entry). Writes restored spec to git as .triform/spec.yaml. Git failures warn but don’t fail the operation — DB state is authoritative. Cannot restore deleted elements.

schema

Get /ops/schema | Auth: Read

Get element input/output schema (MCP tools/list compatible)

Returns type-level port schemas from the TypeRegistry — not instance-specific overrides. Includes direction (input/output), required flag, and JSON schema per port. Useful for understanding what data an element accepts and produces.

send-outreach-campaign

Post /ops/send-outreach-campaign | Auth: Write

Send a tracked outreach email with open/click tracking

Same as send-outreach-email but uses io/email.send_tracked for full open/click/reply tracking. The tracking pixel and link rewriting are handled by the email element. Consent gate is strictly enforced — the contact must have consent_status=“active” or the send is refused. Use for formal campaign sends where engagement metrics matter.

send-outreach-email

Post /ops/send-outreach-email | Auth: Write

Send an outreach email to the card’s contact via the linked email element

Resolves card → lead → contact, ensures the contact exists in data/contacts (auto-upserts with consent_source=“outbound_outreach_sent”), then dispatches io/email.send via the element at spec.email_element_slug. Returns the message_id on success. The email element’s compliance gates (suppression, consent, rate limit) apply — if the contact has opted out, the send is refused with a structured error.

snooze-card

Post /ops/cards/{card_id}/snooze | Auth: Write

Snooze a pursuit for N days — hides it from the Queue until then

Writes metadata.next_action_snooze_until = NOW() + days. The Queue op (list-queue) filters cards where this timestamp is in the future. Passing days = 0 clears the snooze (undo path). v1 is card-level (not per-user). Multiple assignees see the same snooze. If that becomes friction in multi-assignee teams we’ll grow a per-user map here; until then the simpler model keeps the Queue filter a single JSONB comparison.

source

Get /ops/source | Auth: Read

Get any file’s content from the element’s git repository

Reads an arbitrary file from the element’s CAS-backed git tree by its relative path. Same store as readme, just generalized. Path safety: rejects .. traversal, leading /, and null bytes. Use this to view main.py for action elements, asset files for SPAs, etc. Returns empty content (not an error) if the file doesn’t exist.

source_branches

Get /ops/source/branches | Auth: Read

List Source branches for this element

Returns the standard draft/demo/live Source branches, their current commits, and promotion relationships. Use GET /api/{element_path}/ops/source/branches.

source_promote

Post /ops/source/promote | Auth: Write

Promote Source branch forward

Promotes draft to demo or demo to live through the generated element op path. Direct Git pushes to demo/live are blocked by Source policy.

source_repair

Post /ops/source/repair | Auth: Write

Inspect or repair the element Source index

Runs Source repair through the element operation path. Defaults to dry_run=true; set dry_run=false only after reviewing a dry-run report.

source_status

Get /ops/source/status | Auth: Read

Get Source control status for this element

Returns the branch-aware clone URL, checkout commands, current draft commit, child source-link count, portable export summary, Source health, warnings, and auth hints for the addressed element. Use the element-first path: GET /api/{element_path}/ops/source/status.

source_validate

Post /ops/source/validate | Auth: Read

Validate Source branch contents

Validates a Source branch before accepting local Git workflow changes or promotion. Defaults to branch=draft and rejects runtime data, generated output, secret material, and unreadable CAS refs.

stats

Get /ops/stats | Auth: Read

Get aggregate statistics for this element

Health status is computed: error if errors_per_day > 5 or success_rate < 0.8, warning if errors_per_day > 0 or success_rate < 0.95. Firing alerts escalate health to error/warning. Default period is ‘day’. Returns runs_per_day, success_rate, avg_duration_ms, and more.

summarize-call

Post /ops/summarize-call | Auth: Write

Run a brain turn over a call transcript and append the summary to the card

Reads circle_*.call_segments for a call_id, concatenates them into a transcript (speaker-tagged), feeds it to the platform brain with the post-call-summary prompt, and appends the result as a note on the card the call was placed from. Useful for quickly getting a 3-bullet recap + next action after a sales call.

tree

Get /ops/tree | Auth: Read

Get the element’s position in the graph — ancestors, children, references, and subtree statistics

Uses per-circle ElementGraph cache for O(1) lookups. Returns ancestors (containment chain), children (direct), members (references), referenced_by (reverse refs), attachments, and subtree stats. Default depth is 3, max is 10. Pass ?include_metadata=true for name/state on each node.

unarchive-card

Post /ops/cards/{card_id}/unarchive | Auth: Write

Restore an archived card to the board

Restores an archived card, making it visible again in default list-cards results. The card retains all its fields and references. Position defaults to the bottom of the card’s original column.

update

Patch /ops/update | Auth: Write

Update element

Partial update — send only the fields you want to change. spec, name, and intention are all independently optional. spec MUST be a JSON object when present; deep-merged into the existing spec by default. Empty {"spec":{}} preserves existing spec content but still records a new version (no-op for content, not for version state). To clear/replace the entire spec wholesale send {"spec":{...},"deep":false}. List-typed spec fields use replace semantics (the patch list replaces the existing list, no array merging). Coordinates Git + DB writes. Slug cannot be changed after creation.

update-card

Put /ops/cards/{card_id} | Auth: Write

Update a pursuit card

Updates any fields on a pursuit card. Only provided fields are changed. Pursuit bonds (organization_id, contact_ids), outcome, and outcome_reason can all be updated here.

update-column

Put /ops/columns/{column_id} | Auth: Write

Update column name, phase, WIP limit, or position

Updates column properties. Changing phase does NOT change the phase of cards in the column — card phase is independent. Only provided fields are updated. Set wip_limit to 0 to remove the limit.

update-prospect-metadata

Post /ops/update-prospect-metadata | Auth: Write

Write research findings back onto an inbox card — replaces data/leads.upsert for agent write-back

The op research agents (helm) call to layer enrichment + approach_plan onto a card in the inbox. Idempotent. Shallow-merges the enrichment JSONB so multiple research passes accumulate without clobbering. Replaces the speculative data/leads.upsert flow. A card_id is required — research is always scoped to a specific pursuit. For ICP-driven discovery where no card exists yet, the agent calls intake-prospect (or mass-intake-prospects) instead.

update-quote-status

Post /ops/quotes/{quote_id}/status | Auth: Write

Advance a quote through its lifecycle

Flips status; stamps sent_at on first transition to ‘sent’, responded_at on first transition to ‘accepted’ or ‘rejected’. Timestamps are set-once — repeating a status transition does not re-stamp (preserves the original moment). ‘withdrawn’ is our side pulling the quote; ‘expired’ fires when valid_until passes without a response (can be set manually or by a future cron).

update_meta

Patch /ops/update_meta | Auth: Write

Update element metadata (lightweight merge — does NOT bump version or snapshot spec)

Shallow JSONB merge into element.meta. Top-level keys in the provided value replace existing meta values; other keys are preserved. Used for UI metadata like canvas positions, panel state, viewer preferences. Wire-shape op_name is update_meta (distinct from update) so SSE subscribers + the cache auto-invalidator can distinguish lightweight metadata changes from spec edits without inspecting the payload. The MutatingElementStore wrapper stamps this op_name on the lifecycle event emitted by update_element_meta storage calls.

version

Get /ops/version | Auth: Read

Get current version or full history

Returns current version by default. Pass ?history=true for full version history (up to ?limit=N, default 50). Versions are backed by the element_versions table. Every spec update creates a new version entry.

Error Codes

CodeClassRetryableDescription
SALES_BOARD_WIP_EXCEEDEDlimitnoCard move or create would exceed the target column’s wip_limit
SALES_BOARD_COLUMN_NOT_FOUNDvalidationnocolumn_id does not match any column on this sales board
SALES_BOARD_CARD_NOT_FOUNDvalidationnoCard does not exist or belongs to a different board
SALES_EMAIL_ELEMENT_NOT_FOUNDvalidationnospec.email_element_slug does not resolve to a valid io/email element in this circle
SALES_PHONE_ELEMENT_NOT_FOUNDvalidationnospec.phone_number_element_slug does not resolve to a valid io/phone-number element in this circle
SALES_CONSENT_REFUSEDvalidationnoContact has opted out — outreach send refused by the email element’s compliance gate
SALES_ICP_NOT_FOUNDvalidationnoingest-icp-matches: icp_id or icp_slug does not resolve to an ICP element in this circle
SALES_ICP_AMBIGUOUSvalidationnoingest-icp-matches: icp_id and icp_slug both omitted and circle has >1 ICP elements — pass icp_id or icp_slug to disambiguate
SALES_QUOTE_INVALID_TRANSITIONvalidationnoupdate-quote-status: requested status transition is not valid for the current quote state
SALES_LINKEDIN_TOUCH_NOT_FOUNDvalidationnoapprove-linkedin-touch: touch_id does not exist or is already sent
SALES_COMPOSE_NO_CHANNELSvalidationnocompose-message-channels: no actionable messaging channels for this card’s contact
SALES_PROMPT_NOT_FOUNDvalidationnocompose-message-draft: prompt_slug does not resolve to a prompt element in this circle

Observability

Defined for this element

Metrics

  • sales_pursuit_opened_count
  • sales_pursuit_converted_count
  • sales_outreach_sent_count
  • sales_outreach_response_received_count
  • sales_deal_closed_count
  • sales_deal_value_eur
  • sales_pipeline_forecast_queried_count
  • sales_time_in_stage_ms

Events

  • sales.pursuit.opened
  • sales.pursuit.converted
  • sales.outreach.sent
  • sales.outreach.response_received
  • sales.deal.closed
  • sales.pipeline_forecast.queried
  • sales.stage.advanced

Pricing / cost

Platform default

Operation costs

  • create: free
  • update: free
  • delete: free
  • get: free
  • list: free
  • invoke: 10000 micro-AU
  • tool_use: free

Set it up

Board Nametext
Target Industrytext
Regiontext
Target Rolestags
Min Employeesnumber
Max Employeesnumber