Skip to main content

CRM: Custom Fields, Custom Forms, Deals & Pipelines

This guide covers the extensibility and workflow automation features of the Semaswift CRM:

  • Custom Fields — extend the built-in Contact, Account, Deal and Lead schemas with org-specific attributes.
  • Custom Forms — versioned form templates that collect structured data from agents and automatically write values back to CRM records.
  • Deals & Deal Stages — track revenue opportunities through a configurable sales funnel.
  • Pipeline Definitions — author multi-stage, action-driven workflows for contacts or deals.
  • Pipeline Runs — execute pipelines against individual contacts or deals, with full pause/resume/restart control.

All endpoints require a user JWT (Authorization: Bearer {user_jwt}) unless otherwise noted.


1. Custom Fields​

Custom fields let organisations add their own attributes to the four CRM object types: contact, account, deal, and lead. Fields are defined once (a definition) and then values are stored on each individual record.

1.1 Field types​

field_typeStored asValidation options
textUTF-8 string—
numberInteger{"min": N, "max": N}
currencyNumber{"min": N, "max": N, "currency": "USD"}
dateISO 8601 date string (YYYY-MM-DD)—
datetimeISO 8601 datetime string—
booleantrue / false—
urlValidated URL string—
emailValidated email address—
picklistSingle choiceOptions must be declared in picklist_options
multiselectOne or more choicesOptions must be declared in picklist_options

Immutability: key and field_type cannot be changed after creation. To change either, archive the old field and create a new one.

1.2 Create a custom field definition​

POST /api/v1/crm/custom-fields
Authorization: Bearer {user_jwt}
Content-Type: application/json

Request:

{
"object_type": "contact",
"key": "annual_revenue",
"label": "Annual Revenue",
"field_type": "currency",
"validation_rules": { "min": 0, "currency": "USD" },
"section": "Company Info",
"position": 1
}
FieldRequiredNotes
object_typeyesOne of contact, account, deal, lead
keyyesMachine-readable ID. Pattern: ^[a-z][a-z0-9_]{0,63}$. Unique per org + object_type. Immutable
labelyesDisplay name (1–150 chars)
field_typeyesSee table above. Immutable
picklist_optionsyes (picklist/multiselect only){"options": [{"value": "opt_a", "label": "Option A"}, ...]}
validation_rulesnoType-specific JSON object
sectionnoGroups related fields in the UI
positionnoDisplay order within the section (lower = first)

Response (200):

{
"data": {
"id": 42,
"organization_id": 7,
"object_type": "contact",
"key": "annual_revenue",
"label": "Annual Revenue",
"field_type": "currency",
"picklist_options": null,
"validation_rules": { "min": 0, "currency": "USD" },
"section": "Company Info",
"position": 1,
"is_active": true,
"created_at": "2026-06-08T10:00:00Z",
"updated_at": "2026-06-08T10:00:00Z"
},
"meta": { "request_id": "req_abc123" }
}

1.3 Picklist / multiselect fields​

For picklist and multiselect fields, picklist_options is required and must follow this structure:

{
"options": [
{ "value": "smb", "label": "Small Business" },
{ "value": "mid_market", "label": "Mid-Market" },
{ "value": "enterprise", "label": "Enterprise" }
]
}

Both value (machine-readable, used in stored data) and label (display) are required on every option. Empty strings are rejected.

1.4 List, update, and archive​

MethodPathDescription
GET/api/v1/crm/custom-fields/{id}Get a single definition
GET/api/v1/crm/custom-fieldsList all definitions. Query params: object_type=, section=, include_archived=true
PATCH/api/v1/crm/custom-fields/{id}Update mutable fields: label, picklist_options, validation_rules, section, position
POST/api/v1/crm/custom-fields/{id}:archiveArchive a field (is_active = false). Existing values are preserved
POST/api/v1/crm/custom-fields:reorderBatch-update sections and positions

List results are ordered by (section ASC, position ASC). Archived fields are excluded unless include_archived=true.

Batch reorder​

POST /api/v1/crm/custom-fields:reorder
Content-Type: application/json
{
"fields": [
{ "id": 42, "section": "Company Info", "position": 1 },
{ "id": 43, "section": "Company Info", "position": 2 },
{ "id": 44, "section": "Lead Details", "position": 1 }
]
}

Fields not included in the list keep their current section and position.

1.5 Using custom field values on records​

Custom field values are stored in the JSONB custom_fields column on contact and account records. Pass them in CreateContact, UpdateContact, CreateAccount, and UpdateAccount requests:

{
"first_name": "Jane",
"last_name": "Doe",
"custom_fields": {
"annual_revenue": 500000,
"segment": "enterprise"
}
}

The service validates each key exists as an active custom field definition for the contact object type, and validates the value against the field's validation_rules. Unknown keys and type mismatches return 400 INVALID_ARGUMENT.


2. Custom Forms​

Custom forms are versioned, reusable templates that define a structured set of fields for agents to fill out. When a form is submitted, the values are stored as an immutable submission and can optionally be written back to contact or account custom fields.

2.1 Key concepts​

ConceptDescription
Form familyAll versions of a form share the same name. The name is immutable across versions
VersionMonotonically increasing integer per family (v1, v2, …). CreateDraftFromPublished bumps it
Statusdraft → published → archived. Only published forms accept submissions
Write-backA form can automatically write a submitted value into a contact's or account's custom field in the same transaction

2.2 Form field kinds​

kindWhat it is
inlineField defined directly in the form schema. Caller supplies type, validation, picklist_options
referenceField sourced from a custom field definition (custom_field_definition_id). Picklist options are snapshot-resolved at publish time
section_headerNon-data UI divider. Does not produce a value in submissions. Does not count toward the "at least one field" publish rule

2.3 Inline field types​

text, long_text, number, currency, rating, boolean, date, datetime, url, email, phone, picklist, multi_picklist (alias: multiselect), file_upload, signature.

2.4 Create a form​

POST /api/v1/crm/forms
Authorization: Bearer {user_jwt}
Content-Type: application/json

Request:

{
"name": "KYC Verification",
"description": "Collect identity documents from the contact.",
"schema": {
"fields": [
{
"key": "id_type",
"kind": "inline",
"label": "ID Type",
"type": "picklist",
"required": true,
"picklist_options": [
{ "value": "passport", "label": "Passport" },
{ "value": "national_id","label": "National ID" },
{ "value": "drivers", "label": "Driver's Licence" }
]
},
{
"key": "id_number",
"kind": "inline",
"label": "ID Number",
"type": "text",
"required": true,
"validation": { "max_length": 50 }
},
{
"key": "id_scan",
"kind": "inline",
"label": "Scan / Photo of ID",
"type": "file_upload",
"required": true,
"validation": { "max_files": 2, "max_size_mb": 5, "allowed_mime": ["image/jpeg","image/png","application/pdf"] }
},
{
"key": "segment",
"kind": "reference",
"label": "Segment",
"custom_field_definition_id": 43
}
],
"write_back": [
{
"field_key": "segment",
"target": "contact",
"custom_field_definition_id": 43
}
]
}
}

The form is created in draft status with version: 1. New forms always start here — submissions are rejected against drafts.

Response (200) returns the full FormDefinition object.

2.5 Form field structure​

FieldKindRequiredNotes
keyallyesPattern ^[a-z][a-z0-9_]{0,99}$. Unique within the form. Stable identifier used in submission values and write-back
kindallyesinline, reference, or section_header
labelinline, referenceyesDisplay label
label_overridereferencenoOverride the referenced custom field's own label
requiredinline, referencenoDefaults to false. Section headers never required
typeinlineyesSee §2.3
validationinlinenoType-specific: {"max_length": N} for text; {"min": N, "max": N} for number; {"max_files": N, "max_size_mb": N, "allowed_mime": [...]} for file_upload
custom_field_definition_idreferenceyesMust resolve to an active custom field on the relevant object type
picklist_optionsinline picklist/multi_picklistyesArray of {value, label} objects

2.6 Write-back entries​

Write-back makes a submission automatically update a contact's or account's custom field in the same database transaction. Failures (validation error, field archived, no subject ID) are reported per-entry in the submission response and do not abort the submission.

{
"field_key": "segment",
"target": "contact",
"custom_field_definition_id": 43
}
FieldDescription
field_keyForm field whose submitted value is the data source
targetcontact or account. Resolved from the submission's contact_id / account_id
custom_field_definition_idThe custom field to write into. Must apply to the matching object_type

2.7 Publish a form​

POST /api/v1/crm/forms/{id}/publish
Authorization: Bearer {user_jwt}

Publishing:

  • Validates the form against the full ruleset
  • Snapshots picklist options from any reference-kind fields into the schema
  • Flips status to published — the form now accepts submissions
  • Makes the schema immutable

Fails with FAILED_PRECONDITION if the form has no non-section fields (ErrFormEmpty), is not in draft status, or any referenced form action (in a pipeline) points to a non-published form.

2.8 Edit a published form (versioning)​

You cannot patch a published form directly. To make changes:

  1. Fork a new draft from the published form:
POST /api/v1/crm/forms/{id}/drafts

This creates a new draft at version = N+1 with the same name. The published v1 remains live and in-flight submissions still target it.

  1. Patch the draft (schema, description):
PATCH /api/v1/crm/forms/{id}
  1. Publish the new draft.

Existing submissions against earlier versions are unaffected — they embed a schema_snapshot at submit time.

2.9 Archive a form​

POST /api/v1/crm/forms/{id}/archive

Archived forms reject new submissions. Historical submissions remain queryable. Idempotent.

2.10 Delete a form​

DELETE /api/v1/crm/forms/{id}

Only draft or archived forms with no submissions can be deleted. Forms with any submissions must be archived instead. Returns FAILED_PRECONDITION (ErrFormHasSubmissions) otherwise.

2.11 List forms​

GET /api/v1/crm/forms?status=published&name=KYC+Verification&page=1&per_page=50

Results are ordered by name ASC, version DESC — the latest version of each family appears first.

2.12 Submit a form​

POST /api/v1/crm/forms/{form_definition_id}/submissions
Authorization: Bearer {user_jwt}
Content-Type: application/json

Request:

{
"form_definition_id": 10,
"contact_id": 501,
"values": {
"id_type": "passport",
"id_number": "AB123456",
"id_scan": ["upload_7f3a1b2c"],
"segment": "enterprise"
}
}
FieldRequiredNotes
form_definition_idyesMust match the path parameter. Form must be published
contact_idyes**Exactly one of contact_id / account_id must be set
account_idyes*
context_typenoPolymorphic source tag, e.g. "pipeline_run_action" (up to 40 chars)
context_idnoSource ID within context_type
valuesyesKeys must match form field keys. Required fields must be present

Response (200):

{
"data": {
"id": 99,
"organization_id": 7,
"form_definition_id": 10,
"form_version": 1,
"schema_snapshot": { "fields": [...], "write_back": [...] },
"values": {
"id_type": "passport",
"id_number": "AB123456",
"id_scan": ["upload_7f3a1b2c"],
"segment": "enterprise"
},
"contact_id": 501,
"account_id": 0,
"context_type": "",
"context_id": 0,
"write_back_result": [
{
"target": "contact",
"field_key": "segment",
"custom_field_definition_id": 43,
"status": "applied",
"old_value": null,
"new_value": "enterprise"
}
],
"submitted_by_user_id": 17,
"submitted_at": "2026-06-08T11:00:00Z"
}
}

write_back_result[].status values: applied, skipped (field archived or no subject matched), failed (value failed custom-field validation). Write-back failures do not roll back the submission.

2.13 Retrieve and list submissions​

MethodPathDescription
GET/api/v1/crm/form-submissions/{id}Get a single submission (includes schema snapshot)
GET/api/v1/crm/form-submissionsPaginated list. Filters: form_definition_id=, contact_id=, account_id=, context_type=, context_id=
DELETE/api/v1/crm/form-submissions/{id}Soft-delete (retained for audit; excluded from list results)

3. Deals & Deal Stages​

Deals represent sales opportunities. Each deal is linked to at least one contact or account and moves through a configurable set of pipeline stages.

3.1 Default deal stages​

Six stages are seeded automatically when an organisation is created:

sort_orderNameDefault probability
0Prospecting10%
1Qualification20%
2Proposal50%
3Negotiation75%
4Closed Won100%
5Closed Lost0%

These can be renamed, reordered, or replaced. You can also create entirely new stages.

3.2 Manage deal stages​

POST /api/v1/crm/deal-stages
Authorization: Bearer {user_jwt}
Content-Type: application/json

Request:

{
"name": "Proof of Concept",
"description": "Customer is running a technical evaluation",
"sort_order": 2,
"probability": 40
}
FieldRequiredNotes
nameyesMust be non-empty
probabilityyesInteger 0–100. Copied to a deal when the stage is assigned
descriptionnoHuman context for the stage
sort_ordernoLower values appear first; ties broken by creation time

Other stage endpoints:

MethodPathDescription
GET/api/v1/crm/deal-stages/{id}Get a single stage
GET/api/v1/crm/deal-stagesList all active stages, sorted by sort_order ASC
PATCH/api/v1/crm/deal-stages/{id}Update name, description, probability. Use ReorderDealStages to change ordering
DELETE/api/v1/crm/deal-stages/{id}Soft-delete. Fails with FAILED_PRECONDITION (ErrDealStageInUse) if the stage has open deals
POST/api/v1/crm/deal-stages/reorderAtomic batch reorder

Reorder stages​

POST /api/v1/crm/deal-stages/reorder
Content-Type: application/json
{
"stages": [
{ "id": 1, "sort_order": 0 },
{ "id": 3, "sort_order": 1 },
{ "id": 2, "sort_order": 2 }
]
}

Stages not included retain their current sort_order.

3.3 Create a deal​

POST /api/v1/crm/deals
Authorization: Bearer {user_jwt}
Content-Type: application/json

Request:

{
"title": "Acme Corp — Enterprise Plan",
"description": "Annual contract renewal + upsell to 500 seats",
"stage_id": 2,
"contact_id": 501,
"account_id": 101,
"owner_id": 17,
"currency": "USD",
"probability": 50,
"expected_close_date": "2026-09-30T00:00:00Z"
}

At least one of contact_id or account_id must be supplied. The caller must have write permission on the referenced parent object.

FieldRequiredNotes
titleyes1–255 chars
contact_id or account_idyes (one or both)At least one is mandatory
stage_idnoLeave unset to create an un-staged deal
owner_idnoDefaults to the authenticated user
currencynoISO 4217 code, e.g. "USD"
probabilityno0–100. Defaults to the stage's default probability if stage_id is set
expected_close_datenoUsed for pipeline forecasting
source_contact_idnoLinks this deal to the originating lead contact (e.g. from ConvertLead)

3.4 The Deal object​

{
"id": 201,
"organization_id": 7,
"title": "Acme Corp — Enterprise Plan",
"description": "Annual contract renewal + upsell to 500 seats",
"stage_id": 2,
"contact_id": 501,
"account_id": 101,
"owner_id": 17,
"currency": "USD",
"value": 24000.00,
"probability": 50,
"expected_close_date": "2026-09-30T00:00:00Z",
"outcome": "",
"close_reason": "",
"closed_at": null,
"closed_by": null,
"created_by": 17,
"source_contact_id": 0,
"line_items": [],
"created_at": "2026-06-08T10:00:00Z",
"updated_at": "2026-06-08T10:00:00Z"
}

value is read-only — it is automatically computed as SUM(quantity × unit_price) across all line items. outcome is "" (open), "won", or "lost".

Note: line_items is populated only on GetDeal; it is empty in list responses for performance.

3.5 Line items​

Line items are the products or services in a deal. Their quantities and unit prices drive the deal's value.

POST /api/v1/crm/deals/{deal_id}/line-items
Content-Type: application/json
{
"name": "Enterprise Licence — 500 seats",
"description": "Annual seat licence",
"quantity": 500,
"unit_price": 48.00,
"sort_order": 1
}

Response includes the new LineItem and the deal's updated value (500 × 48.00 = 24000.00).

MethodPathDescription
GET/api/v1/crm/deals/{deal_id}/line-itemsList all line items, ordered by sort_order ASC
PATCH/api/v1/crm/deals/{deal_id}/line-items/{item_id}Update a line item. Deal value is recomputed
DELETE/api/v1/crm/deals/{deal_id}/line-items/{item_id}Hard delete (not soft-deleted). Deal value is recomputed

3.6 Update a deal​

PATCH /api/v1/crm/deals/{id}
Content-Type: application/json

All fields are optional. Supply only the fields you want to change. To move a deal to a different stage, set stage_id. To un-stage a deal, omit stage_id (proto3 optional semantics handle the nil case server-side).

3.7 Close a deal​

Deals have two terminal transitions: won and lost. Both require a non-empty close_reason.

POST /api/v1/crm/deals/{id}/won
Content-Type: application/json
{ "close_reason": "Signed 12-month contract" }
POST /api/v1/crm/deals/{id}/lost
Content-Type: application/json
{ "close_reason": "Budget cut — revisit Q1 2027" }

Both transitions:

  • Set outcome ("won" or "lost"), closed_at, and closed_by
  • Write a crm.deal.won / crm.deal.lost activity entry on the parent contact/account
  • Are terminal — a closed deal cannot be re-opened or re-closed (FAILED_PRECONDITION)

3.8 List and filter deals​

GET /api/v1/crm/deals?page=1&per_page=50

Filter query parameters (all optional):

ParameterDescription
filter.contact_idOnly deals linked to this contact
filter.account_idOnly deals linked to this account
filter.stage_idsComma-separated list of stage IDs
filter.owner_idOnly deals owned by this user
filter.outcome"" (all), "open", "won", or "lost"
filter.expected_close_fromISO 8601 datetime
filter.expected_close_toISO 8601 datetime
filter.value_minMinimum deal value
filter.value_maxMaximum deal value
filter.source_contact_idDeals sourced from this lead contact

Defaults to all deals (open + closed), ordered by created_at DESC, 50 per page.


4. Pipeline Definitions​

Pipelines are versioned, multi-stage workflow templates that drive structured processes for contacts or deals — KYC, onboarding sequences, deal qualification, etc. A pipeline definition describes the template; actual executions are tracked as Pipeline Runs (§5).

4.1 Subject types​

subject_typeEnrollment APIAuto-enroll support
contactPOST /api/v1/crm/contacts/{contact_id}/pipeline-runsYes — via auto_enroll_contact_stage_id
dealPOST /api/v1/crm/deals/{deal_id}/pipeline-runsNo

subject_type is immutable across versions — a contact pipeline can never be re-published as a deal pipeline.

4.2 Schema structure​

The pipeline schema is a JSON object with this shape:

{
"entry_stage_key": "intake",
"stages": [
{
"key": "intake",
"name": "Intake",
"terminal": false,
"actions": [
{
"key": "kyc_form",
"type": "form",
"required": true,
"config": {
"form_definition_id": 10
}
},
{
"key": "verify_checklist",
"type": "checklist",
"required": true,
"config": {
"items": [
{ "key": "id_verified", "title": "ID document verified" },
{ "key": "address_verified", "title": "Address verified" }
]
}
}
],
"branches": [
{
"expression": "{{kyc_form.id_type}} == 'passport'",
"target": "passport_review"
},
{
"expression": "default",
"target": "standard_review"
}
]
},
{
"key": "passport_review",
"name": "Passport Review",
"terminal": false,
"actions": [
{
"key": "create_deal_from_kyc",
"type": "create_deal",
"required": false,
"config": {
"title_template": "KYC Deal — {{kyc_form.id_number}}",
"deal_stage_id": 1
}
}
],
"branches": [
{ "expression": "default", "target": "completed" }
]
},
{
"key": "standard_review",
"name": "Standard Review",
"terminal": false,
"actions": [],
"branches": [
{ "expression": "default", "target": "completed" }
]
},
{
"key": "completed",
"name": "Completed",
"terminal": true,
"actions": [],
"branches": []
}
]
}

Schema rules​

  • entry_stage_key must reference an existing stage key.
  • Stage keys match ^[a-z][a-z0-9_]{0,99}$ and must be unique across the pipeline.
  • Action keys match the same pattern and must be unique within their stage.
  • Every non-terminal stage must have at least one branch. The last branch must use the expression "default".
  • Branch targets must reference existing stage keys. The stage graph must be acyclic and fully reachable from entry_stage_key.

4.3 Action types​

typeWhen it runsConfig fields
formOn stage entry — materialises an action row the agent fills out via SubmitFormform_definition_id (required; form must be published)
checklistOn stage entry — materialises a checklist the agent ticks offitems: ordered list of { "key": "...", "title": "..." }
create_dealRuns automatically when the stage is enteredtitle_template, amount_var (optional variable reference), deal_stage_id
send_notificationRuns automatically when the stage is enteredrecipient, template_id

required: true means the action must complete successfully before the stage can advance. A non-required action failure does not fail the run.

4.4 Variables and interpolation​

As actions complete, their results are merged into the run's variables bag under the action's key:

{
"kyc_form": {
"id_type": "passport",
"id_number": "AB123456",
"segment": "enterprise"
},
"verify_checklist": {
"id_verified": true,
"address_verified": true
}
}

Branch expressions and action config templates interpolate from this bag using {{action_key.path}} syntax:

"{{kyc_form.id_type}} == 'passport'"
"title_template": "KYC Deal — {{kyc_form.id_number}}"

4.5 Create a pipeline​

POST /api/v1/crm/pipelines
Authorization: Bearer {user_jwt}
Content-Type: application/json

Request:

{
"name": "KYC Onboarding",
"description": "End-to-end KYC verification for new contacts",
"subject_type": "contact",
"schema": { ... },
"auto_enroll_contact_stage_id": 0
}
FieldRequiredNotes
nameyes1–255 chars. Immutable across versions
subject_typeyescontact or deal. Immutable across versions
schemayesFull stage/action/branch graph. Validated on creation
descriptionnoHuman-readable summary
auto_enroll_contact_stage_idnoContact-subject only. Set to a contact stage ID to auto-enroll contacts when they enter that stage. 0 = disabled

The pipeline is created in draft status.

4.6 Publish a pipeline​

POST /api/v1/crm/pipelines/{id}/publish

Re-validates the full schema ruleset (stage graph, action config requirements, referenced form publication status) and flips status to published. Enrollments are now accepted.

4.7 Versioning workflow​

StepAction
1Fork a new draft: POST /api/v1/crm/pipelines/{id}/drafts (bumps version, status = draft)
2Edit the draft: PATCH /api/v1/crm/pipelines/{id} (description, schema, auto-enroll trigger)
3Publish: POST /api/v1/crm/pipelines/{id}/publish

In-flight runs against older versions continue against their definition snapshot — they are unaffected by new versions.

4.8 Archive and delete​

ActionDescription
POST /api/v1/crm/pipelines/{id}/archiveTransitions to archived. New enrollments rejected; in-flight runs continue. Idempotent
DELETE /api/v1/crm/pipelines/{id}Soft-deletes a draft or archived pipeline. Fails with FAILED_PRECONDITION (ErrPipelineHasRuns) if the pipeline has ever been enrolled

4.9 List pipelines​

GET /api/v1/crm/pipelines?status=published&subject_type=contact&name=KYC+Onboarding

Ordered by name ASC, version DESC. Pagination via page and per_page.


5. Pipeline Runs​

A pipeline run is one execution of a pipeline definition against a single subject (a contact or deal). Each run tracks the current stage, an accumulating variables bag, and an ordered timeline of materialised actions.

5.1 Run status values​

StatusDescription
runningActively progressing. Pending actions await completion
pausedFrozen by PausePipelineRun. No transitions until resumed
completedTerminal — run reached a stage with terminal: true
cancelledTerminal — manually cancelled. cancel_reason is populated
failedTerminal — an action failed unrecoverably. failure_reason is populated

5.2 Enroll a contact​

POST /api/v1/crm/contacts/{contact_id}/pipeline-runs
Authorization: Bearer {user_jwt}
Content-Type: application/json
{
"pipeline_definition_id": 5
}

The pipeline's subject_type must be "contact". If the contact already has a running or paused run for this pipeline, enrollment returns FAILED_PRECONDITION (ErrPipelineAlreadyEnrolled). Terminal runs (completed, cancelled, failed) do not block re-enrollment.

Response (200) returns a PipelineRun object (see §5.4).

5.3 Enroll a deal​

POST /api/v1/crm/deals/{deal_id}/pipeline-runs
Authorization: Bearer {user_jwt}
Content-Type: application/json
{
"pipeline_definition_id": 8
}

The pipeline's subject_type must be "deal".

5.4 The PipelineRun object​

{
"id": 301,
"organization_id": 7,
"pipeline_definition_id": 5,
"definition_version": 2,
"subject_type": "contact",
"subject_id": 501,
"status": "running",
"current_stage_key": "intake",
"variables": {},
"enrolled_by_user_id": 17,
"enroll_source": "manual",
"cancel_reason": "",
"failure_reason": "",
"paused_at": null,
"started_at": "2026-06-08T11:00:00Z",
"completed_at": null,
"cancelled_at": null,
"failed_at": null
}
FieldDescription
definition_versionThe pipeline version pinned at enrollment. Edits to later versions do not affect this run
current_stage_keyStage the run is currently in
variables{action_key: result} bag accumulated as actions complete. Drives branch evaluation
enroll_source"manual" (user called Enroll) or "auto" (triggered by a contact stage transition)

5.5 Retrieve and list runs​

MethodPathDescription
GET/api/v1/crm/pipeline-runs/{id}Get a single run
GET/api/v1/crm/pipeline-runsPaginated list. Filters: pipeline_definition_id=, subject_type=, subject_id=, status=

5.6 Action timeline​

Each materialised action in a run becomes a PipelineRunAction row. Query the full ordered timeline:

GET /api/v1/crm/pipeline-runs/{run_id}/actions

Each row includes:

{
"id": 1001,
"pipeline_run_id": 301,
"stage_key": "intake",
"action_key": "kyc_form",
"action_type": "form",
"required": true,
"status": "completed",
"form_submission_id": 99,
"deal_id": 0,
"engagement_id": 0,
"result": {
"id_type": "passport",
"id_number": "AB123456"
},
"completed_by_user_id": 17,
"completed_at": "2026-06-08T11:10:00Z",
"error_message": ""
}
FieldDescription
statuspending, in_progress, completed, skipped, failed
form_submission_idPopulated when action_type = "form" and the action completed
deal_idPopulated when action_type = "create_deal" and the deal was created
engagement_idPopulated when action_type = "send_notification"
resultSnapshot of what the engine wrote into run.variables under this action's key

Stage progression​

When all required actions in the current stage complete, the engine evaluates the stage's branches in order. The first truthy branch wins; "default" is the fallback. The run transitions to the target stage and that stage's actions materialise.

5.7 Cancel a run​

POST /api/v1/crm/pipeline-runs/{id}/cancel
Content-Type: application/json
{ "reason": "Customer withdrew application" }

reason is required (min 1 char). Pending action rows are marked skipped. The run transitions to cancelled.

5.8 Pause and resume​

Pause — freezes a running run. No new actions materialise; pending actions that complete record their results but stage transitions are deferred.

POST /api/v1/crm/pipeline-runs/{id}/pause

Resume — lifts the pause and re-evaluates the current stage against the current variables.

POST /api/v1/crm/pipeline-runs/{id}/resume

5.9 Restart from a stage​

Rewinds the run to a given stage, clearing every action that materialised at or after that stage (and removing the variables they contributed). The target stage's actions are re-materialised and the run re-enters running status.

POST /api/v1/crm/pipeline-runs/{id}/restart
Content-Type: application/json
{ "from_stage_key": "intake" }

from_stage_key must reference a stage present in the run's pinned definition snapshot. Works from any run status — terminal runs (completed, cancelled, failed) are revived.


6. Auto-enrollment​

For contact-subject pipelines, you can trigger automatic enrollment whenever a contact enters a specific stage.

When creating or updating a pipeline definition, set auto_enroll_contact_stage_id to a contact stage ID:

{
"auto_enroll_contact_stage_id": 3
}

When a contact transitions into that stage, the pipeline engine automatically enrolls them. Auto-enrolled runs are recorded with enroll_source: "auto". The idempotency constraint still applies — if the contact already has an open run for this pipeline, the auto-enrollment is silently skipped.

Set auto_enroll_contact_stage_id = 0 to disable auto-enrollment. This field must always be 0 on deal-subject pipelines.


7. End-to-end example: KYC pipeline for contacts​

This walkthrough shows how all the pieces fit together.

Step 1 — Define custom fields​

POST /api/v1/crm/custom-fields
{ "object_type": "contact", "key": "kyc_status", "label": "KYC Status",
"field_type": "picklist", "picklist_options": {"options": [
{"value": "pending", "label": "Pending"},
{"value": "approved", "label": "Approved"},
{"value": "rejected", "label": "Rejected"}
]}, "section": "Compliance" }

Step 2 — Create and publish the form​

POST /api/v1/crm/forms
{ "name": "KYC Verification", "schema": { "fields": [...], "write_back": [
{ "field_key": "kyc_status", "target": "contact", "custom_field_definition_id": 42 }
]}}

POST /api/v1/crm/forms/10/publish

Step 3 — Create and publish the pipeline​

POST /api/v1/crm/pipelines
{
"name": "KYC Onboarding", "subject_type": "contact",
"auto_enroll_contact_stage_id": 3,
"schema": {
"entry_stage_key": "review",
"stages": [
{
"key": "review", "name": "Review", "terminal": false,
"actions": [{ "key": "kyc_form", "type": "form", "required": true, "config": { "form_definition_id": 10 }}],
"branches": [
{ "expression": "{{kyc_form.kyc_status}} == 'approved'", "target": "done" },
{ "expression": "default", "target": "rejected" }
]
},
{ "key": "done", "name": "Approved", "terminal": true, "actions": [], "branches": [] },
{ "key": "rejected", "name": "Rejected", "terminal": true, "actions": [], "branches": [] }
]
}
}

POST /api/v1/crm/pipelines/5/publish

Step 4 — A contact enters the auto-enroll stage​

The contact moves into contact stage ID 3. The pipeline engine auto-enrolls them, creating a run (status: running, current_stage_key: "review") and materialising the kyc_form action.

Step 5 — Agent submits the form​

POST /api/v1/crm/forms/10/submissions
{
"form_definition_id": 10, "contact_id": 501,
"context_type": "pipeline_run_action", "context_id": 1001,
"values": { "kyc_status": "approved", "id_type": "passport", "id_number": "AB123456" }
}

The write-back rule fires: contact.custom_fields.kyc_status is set to "approved".

Step 6 — Pipeline advances​

The engine evaluates the review stage's branches against the updated variables. {{kyc_form.kyc_status}} == 'approved' is truthy, so the run transitions to the done stage (terminal). The run status becomes completed.


8. Complete API reference​

Custom Fields​

MethodPathDescription
POST/api/v1/crm/custom-fieldsCreate a custom field definition
GET/api/v1/crm/custom-fields/{id}Get a definition
GET/api/v1/crm/custom-fieldsList definitions (object_type=, section=, include_archived=)
PATCH/api/v1/crm/custom-fields/{id}Update label, picklist options, validation rules, section, position
POST/api/v1/crm/custom-fields/{id}:archiveArchive a field
POST/api/v1/crm/custom-fields:reorderBatch reorder

Custom Forms​

MethodPathDescription
POST/api/v1/crm/formsCreate form definition (draft)
GET/api/v1/crm/forms/{id}Get a form
GET/api/v1/crm/formsList forms (status=, name=)
PATCH/api/v1/crm/forms/{id}Update draft (description, schema)
POST/api/v1/crm/forms/{id}/publishPublish a draft
POST/api/v1/crm/forms/{id}/draftsFork published form into new draft
POST/api/v1/crm/forms/{id}/archiveArchive a form
DELETE/api/v1/crm/forms/{id}Delete draft / archived form (no submissions)
POST/api/v1/crm/forms/{form_definition_id}/submissionsSubmit a form
GET/api/v1/crm/form-submissions/{id}Get a submission
GET/api/v1/crm/form-submissionsList submissions
DELETE/api/v1/crm/form-submissions/{id}Soft-delete a submission

Deals & Stages​

MethodPathDescription
POST/api/v1/crm/deal-stagesCreate deal stage
GET/api/v1/crm/deal-stages/{id}Get deal stage
GET/api/v1/crm/deal-stagesList deal stages (sorted by sort_order)
PATCH/api/v1/crm/deal-stages/{id}Update name, description, probability
DELETE/api/v1/crm/deal-stages/{id}Soft-delete (fails if stage has open deals)
POST/api/v1/crm/deal-stages/reorderAtomic batch reorder
POST/api/v1/crm/dealsCreate deal
GET/api/v1/crm/deals/{id}Get deal (includes line items)
GET/api/v1/crm/dealsList deals (filters: contact, account, stage, owner, outcome, value, dates)
PATCH/api/v1/crm/deals/{id}Update deal
POST/api/v1/crm/deals/{id}/wonClose deal as won
POST/api/v1/crm/deals/{id}/lostClose deal as lost
DELETE/api/v1/crm/deals/{id}Soft-delete deal and line items
POST/api/v1/crm/deals/{deal_id}/line-itemsAdd line item
GET/api/v1/crm/deals/{deal_id}/line-itemsList line items
PATCH/api/v1/crm/deals/{deal_id}/line-items/{item_id}Update line item
DELETE/api/v1/crm/deals/{deal_id}/line-items/{item_id}Hard-delete line item

Pipeline Definitions​

MethodPathDescription
POST/api/v1/crm/pipelinesCreate pipeline definition (draft)
GET/api/v1/crm/pipelines/{id}Get a pipeline
GET/api/v1/crm/pipelinesList pipelines (status=, name=, subject_type=)
PATCH/api/v1/crm/pipelines/{id}Update draft (description, schema, auto-enroll trigger)
POST/api/v1/crm/pipelines/{id}/publishPublish a draft
POST/api/v1/crm/pipelines/{id}/draftsFork published pipeline into new draft
POST/api/v1/crm/pipelines/{id}/archiveArchive a pipeline
DELETE/api/v1/crm/pipelines/{id}Delete draft / archived pipeline (no runs)

Pipeline Runs​

MethodPathDescription
POST/api/v1/crm/contacts/{contact_id}/pipeline-runsEnroll a contact
POST/api/v1/crm/deals/{deal_id}/pipeline-runsEnroll a deal
GET/api/v1/crm/pipeline-runs/{id}Get a run
GET/api/v1/crm/pipeline-runsList runs (pipeline_definition_id=, subject_type=, subject_id=, status=)
GET/api/v1/crm/pipeline-runs/{run_id}/actionsList action timeline for a run
POST/api/v1/crm/pipeline-runs/{id}/cancelCancel a run
POST/api/v1/crm/pipeline-runs/{id}/pausePause a running run
POST/api/v1/crm/pipeline-runs/{id}/resumeResume a paused run
POST/api/v1/crm/pipeline-runs/{id}/restartRestart from a stage key