opbox

The stakeholder.* family (8 verbs). Every verb enters through the one front door: it is capability-checked, permission-checked, scope-checked and audited before it runs (see Security & permissions). Each entry below lists its capability, risk class (which fixes the minimum autonomy level), the minimum caller permission, its availability tier, whether it is idempotent, and any outbound egress.

Upsertstakeholder.companyLink.upsert

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionMember
AvailabilityOn demand (MCP tier 2)
IdempotentYes
EgressNone - in-box (pure Postgres, no outbound call)

{ stakeholderId, companyTableId, companyRowId, companyName, role, status? }. Idempotency key (stakeholder_id, company_table_id, company_row_id): ON CONFLICT DO UPDATE role + status + company_name. The stakeholder is fenced within the workspace. role/status validated against their enums. NB: stakeholder_company_links has NO updated_at column.

Call POST /v/stakeholder.companyLink.upsert with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.companyLink.upsert.

Setstakeholder.favourite.set

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionMember
AvailabilityOn demand (MCP tier 2)
IdempotentNo
EgressNone - in-box (pure Postgres, no outbound call)

Toggle the acting user’s favourite of a CSP entity/individual. { entityType, entityId }. Returns { entityType, entityId, favorited }.

Call POST /v/stakeholder.favourite.set with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.favourite.set.

Setstakeholder.follow.set

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionMember
AvailabilityOn demand (MCP tier 2)
IdempotentNo
EgressNone - in-box (pure Postgres, no outbound call)

Toggle the acting user’s follow of a CSP entity/individual. { entityType, entityId }. Returns { entityType, entityId, following }.

Call POST /v/stakeholder.follow.set with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.follow.set.

Createstakeholder.source.create

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionAdmin
AvailabilityOn demand (MCP tier 2)
IdempotentNo
EgressNone - in-box (pure Postgres, no outbound call)

{ tableId, emailColumnId, nameColumnId? }. The table + columns are fenced within the workspace; the (workspace_id, table_id) UNIQUE maps 23505 -> Conflict (the route’s 409). enabled defaults true.

Call POST /v/stakeholder.source.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.source.create.

Deletestakeholder.source.delete

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionAdmin
AvailabilityOn demand (MCP tier 2)
IdempotentNo
EgressNone - in-box (pure Postgres, no outbound call)

{ id }. DELETE by id + workspace fence; rows_affected==0 -> BadInput (the route’s 404).

Call POST /v/stakeholder.source.delete with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.source.delete.

Disable Allstakeholder.source.disableAll

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionAdmin
AvailabilityAdmin only (CLI/HTTP; never advertised over MCP)
IdempotentYes
EgressNone - in-box (pure Postgres, no outbound call)

(system). No input beyond ctx. Disable every enabled source in the workspace (the CSP-uninstall cleanup). Idempotent: rows_affected==0 -> changed

.

Call POST /v/stakeholder.source.disableAll with a JSON body; returns JSON (or { error, code }). Not advertised over MCP (admin-only); reachable via the CLI and HTTP doors.

Updatestakeholder.source.update

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionAdmin
AvailabilityOn demand (MCP tier 2)
IdempotentNo
EgressNone - in-box (pure Postgres, no outbound call)

{ id, emailColumnId?, nameColumnId?|null, enabled?, lastResolvedAt? }. A partial PATCH by id + workspace fence; rows_affected==0 -> BadInput not-found (the route’s 404). nameColumnId discriminates absent (leave) vs null (un-set) vs string (set). lastResolvedAt is the background-resolver touch leg (ISO timestamp).

Call POST /v/stakeholder.source.update with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.source.update.

Upsertstakeholder.upsert

PropertyValue
Capabilitystakeholder.write
Risk classWrite - autonomy L1
Min. permissionMember
AvailabilityOn demand (MCP tier 2)
IdempotentYes
EgressNone - in-box (pure Postgres, no outbound call)

The find-then-create/update + P2002 race dance collapsed into one INSERT … ON CONFLICT (workspace_id, email) DO UPDATE. Email is normalised lower+trim. The CONDITIONAL MERGE preserves the invariants of the old code: only overwrite the enrichment fields (formatted_address / recipient_notice*) when the NEW value is non-empty (never clobber with a blank), and set company_row_id ONLY when it is currently NULL (never flip a non-null primary-company pointer); identity fields (first/last name, dob, nationality) are set on INSERT only, never updated. created_by_id is bound from the input (a service-context actor, e.g. matter.createdById), NOT the acting user. { email, firstName, lastName, dateOfBirth?, nationality?, formattedAddress?, recipientNoticeName?, recipientNoticeEmail?, formattedRecipientNoticeAddress?, companyRowId?, createdById }.

Call POST /v/stakeholder.upsert with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool stakeholder.upsert.