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
Statusdraftpublishedarchived. 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