Workflows API
Build workflow drafts, set live sending schedules, enroll contacts in bulk, and manage runs and templates through the Goliath developer API
About
Full lifecycle control over workflow automations from your own code: list and inspect workflows, edit drafts, promote them live, pause and resume, trigger runs against records, control in-flight runs, read performance stats, and organize workflows into folders. You can also manage Workflow Tables, example payloads for event workflows, reusable content templates and library files, and apply a saved task or a whole task set to a contact.
All operations go through the developer API gateway — one endpoint, called by
operationId. If you haven’t set up a key yet, start with the
Developer API Overview.
Operations
Reading workflows (READ scope)
| operationId | What it does |
|---|---|
listWorkflows |
List the organization’s workflow automations (paginated, non-archived by default). Narrow by status, domain, name (any-case search) and triggerType; names are not unique, so read every page before calling one row the only match. Returns the group id and channelsAcrossVersions, including channels configured in superseded versions and unpublished drafts. |
getWorkflow |
Fetch one workflow by group id — status, versions, live version, pending draft, draft validation, channels, channelsAcrossVersions, and the group’s live sendWindow. The two channel fields describe different version sets; see below. |
getWorkflowVersionGraph |
Fetch one workflow version including its node/edge graph and configured channels. Webhook authentication secrets are redacted; preserve the returned sentinel when editing. |
listWorkflowRuns |
Page runs of one workflow version with limit (default 20, cap 34) and offset, optionally filtered by status (including NOT_ENROLLED). Returns run ids, stoppedReason, pausedReason, sendFailureCode, and queuedSend with reason and next attempt time. |
getWorkflowStats |
Performance summary for one workflow (by group id) — run counts by outcome, messages sent, reply rate + sentiment split, bounce rate, last run. |
getWorkflowVersionStats |
The same summary scoped to one version, plus sendingCapacity (queuedRuns, nextAttemptAt, reason) for sends waiting on capacity. Null means none are queued. |
listWorkflowPerformance |
Scoreboard — the org’s workflows each with headline stats (runs, active, reply rate, last run) and channelsAcrossVersions. |
getWorkflowThrottleUsage |
Daily send-limit usage for a throttle-backed workflow (usedToday vs dailyCap + history). Null for workflows with no throttle. |
Building and shipping (WRITE scope)
| operationId | What it does |
|---|---|
createWorkflow |
Create a group and its initial DRAFT. Requires name and workflowDomain (CONTACT, DEAL, PROPERTY, APPOINTMENT, or EVENT); optionally seed from a templateId and set the group’s sendWindow. Channels come from the graph. The retired workflowType variable is rejected with 400 unknown_variable. |
createOrReturnWorkflowDraft |
Idempotent “edit” — return the existing DRAFT, or clone the live or most recently paused version into a fresh DRAFT. Webhook authentication secrets are redacted. |
updateWorkflowGraph |
Replace a version’s node/edge graph and triggers — read the graph via getWorkflowVersionGraph, then send back a modified copy. Edit the DRAFT from createOrReturnWorkflowDraft; the response includes workflowGroup.draftValidation (isValid, errors). PAUSED versions are also writable, but that validation still describes the group’s draft, not the paused version you edited. To change only part of a graph, use patchWorkflowGraph. Pass expectedUpdatedAt (the automation’s updatedAt as you read it) so a concurrent save fails with CONFLICT instead of being overwritten. A save that turns off text screening or the stop-on-reply exit is refused until you resend it with acknowledgeGraphWeakening: true, which you set only after the person agreed. |
patchWorkflowGraph |
Edit a version’s graph by sending only the change: graphPatch is a list of ops applied in order, all or nothing — updateNode, addNode (after a node or onEdge), removeNode, addEdge, updateEdge, removeEdge, updateConfig, addTrigger, updateTrigger, removeTrigger. Node and edge ids change on every save, so patch again from the response’s graph. Takes expectedUpdatedAt and acknowledgeGraphWeakening like updateWorkflowGraph. |
renameWorkflow |
Rename a workflow version and optionally update its description. |
promoteWorkflowDraft |
Promote the current DRAFT to ACTIVE (live) and pause the previously-active version. inFlightRunsStrategy decides what happens to that version’s in-flight runs: MIGRATE (the default) moves them onto the new version — a run sitting on a step the draft removed or replaced finishes, and runs held by a workflow pause wake — while PAUSE parks them on the old version instead. pauseInFlightRuns is no longer accepted. |
discardWorkflowDraft |
Discard the current DRAFT, if any. Idempotent. |
restoreWorkflowVersionAsDraft |
Roll back: copy an older, paused version (workflowAutomationId from getWorkflow.versions) into the DRAFT, then publish it with promoteWorkflowDraft. Replaces the current draft; nothing goes live. The most recently published version is refused — resume it with resumeWorkflowVersion. |
rewriteWorkflowDraftTextStep |
The publish dialog’s “Rewrite for me”: reword one DRAFT text step to lower its carrier-filter risk. Pass a getWorkflow.draftValidation.carrierRisk row’s nodeId and its body as expectedBody (medium or high band only). Returns REWRITTEN, NO_BETTER_REWRITE or STEP_CHANGED. Never publishes. |
duplicateWorkflow |
Duplicate a workflow into a fresh, non-running group with the given name, preserving its system category and returning channels. Appointment reminders, integration lead-handling workflows, and AI-employee event routers cannot be duplicated. Create reminders through the Appointments API. |
updateWorkflowPriority |
Set a workflow’s priority against the organization’s other workflows: 1.25 Low, 1.5 Normal, 1.75 High; any other value is refused. Applies immediately, to a live workflow too. |
listWorkflowGraphRevisions |
A workflow’s save history (by group id), newest first: when, who, the version written, and what changed against the previous save. limit defaults to 25, capped at 100. Requires READ. |
previewWorkflowAiText |
Preview what an AI text step’s systemPrompt would write to 1 to 5 contactIds, through the real generation path. Each result carries generatedText or a declineReason. Sends and saves nothing; it makes one model call per contact. |
deleteWorkflow |
Archive the group, pause the active version, and permanently stop its in-flight runs. AI-employee event routers cannot be deleted. Confirm before deleting; restoring the workflow does not restore those runs. |
restoreWorkflow |
Undo a group’s archive so it can be edited again. Does not activate versions or restart stopped runs; use resumeWorkflow when ready. EVENT workflows and appointment reminders cannot be restored this way. |
updateWorkflowSendWindow |
Change the group’s live sending schedule immediately, without a draft or promotion. Every run reads it on its next send, regardless of version. Explicit null restores the platform default. |
applyTaskTemplate |
Apply one TASK template to a contact, creating a task assigned to the acting user and linked to any supplied dealIds. Renders merge fields and spintax; resolves relative timing from now. Optional overrides change this task without editing the template. |
applyTaskSet |
Apply a TASK_SET to a contact in one call. Creates tasks in due order, assigned to the acting user and linked to the contact and optional dealIds. Returns created tasks plus failedStep if a step fails; earlier tasks are kept. |
Reading workflow channels
workflowType has been removed from workflow responses. Channels are
TEXT, EMAIL, both, or an empty list for automation with no messaging.
channelsongetWorkflowreads the most recently published version, even if it is now paused or suspended. It falls back to the pending draft only when nothing has ever been published. Null means the graph could not be read; an empty list can also mean the group has no version.channelsAcrossVersionsincludes every non-deleted version, including superseded versions and unpublished drafts. It is never null and is the field returned by both paginated workflow listings.
A workflow whose old version texted and whose latest publication only emails
can therefore return ["TEXT", "EMAIL"] in a listing and ["EMAIL"] in
getWorkflow.channels. Neither field proves messages are being sent: check
status separately.
Authoring a graph
Use updateWorkflowGraph’s discovery detail for the current step vocabulary
and configuration shapes for each workflow domain. Preserve the serialized
shape returned by getWorkflowVersionGraph; a graph contains nodes,
edges, triggers, and graph-level config. Omitting a top-level key
preserves its stored value; sending triggers replaces that collection, so
an existing trigger left out of the array is detached. A trigger needs only
config. A text/email step without channel communication configuration gets
the editor default; replace an UNSET sender before promotion.
DEAL graphs can write a deal custom field, add a note, create a task or
appointment linked to the deal, and enroll the deal in another DEAL workflow.
send_webhook_action is available on CONTACT, PROPERTY, DEAL, and APPOINTMENT
graphs. CONTACT graphs also support send_mail_action through the
organization’s connected mail account; a refused mailing is recorded on the
run and the run continues. Read discovery for each step’s required config.
EVENT graphs support ai_conditional (Ask AI). Its config.context.event
selects { mode: "whole" } for the entire webhook body or
{ mode: "fields", paths: ["call.outcome"] } for 1–25 accessor paths.
An empty event selection is refused; it is not a selection of record ids.
CONTACT, PROPERTY, and DEAL conditionals use their domain’s full saved-filter
vocabulary: discover it with listFilterFieldNames, then request the fields
you need with listFilterFields. Those discovery operations accept
CONTACT, PROPERTY, and DEAL; their absence for another domain does not
establish which conditions that domain can evaluate.
Merge fields are exact and case-sensitive, and depend on the node’s rendering
context. For example, a CONTACT text body can use {{firstName}} and
{{contactPropertyAddress}}; {{contact.firstName}} is not a supported
namespace. EVENT bodies use accessors into the webhook payload instead.
Read the per-domain vocabulary in discovery before authoring a body.
config.exclusiveEnrollment: true stops the record’s runs in other workflows
when it enters this one. It leaves runs from another version of this same
workflow alone and has no effect on EVENT workflows.
config.activation.triggerEligibleRecordsNow is retired and has no effect.
Do not set it to enroll existing matches. Use triggerWorkflowRun for one
record or enrollInWorkflow for a contact selection.
A manual trigger is not the only way a workflow can receive records: steps in
other workflows can enroll them. getDeletionImpact(targetKind: WORKFLOW_GROUP, targetId: ...) reports reference counts, not the names of feeding workflows;
read their graphs to identify those references. To narrow what a workflow
acts on, edit and promote a draft with the appropriate conditional; pausing
alone does not change its criteria.
CONTACT and APPOINTMENT graphs configure messaging under
config.communicationConfig.text and .email; both can be present at once.
Each channel has its own sender. The sending schedule belongs to the group,
outside the graph. Set each required sender before promoting; UNSET fails
draft validation.
A text_action or email_action step can set config.senderOverride to
replace the workflow’s sender for that step. Omit it to inherit the workflow
sender. Text overrides support POINT_PERSON, PHONE with
fromPhoneNumberId, or RANDOM_PHONE with fromPhoneNumberIds. Email
overrides support POINT_PERSON, IDENTITY with fromEmail and
fromClientId, or RANDOM_IDENTITY with identities. Both also accept
UNSET, which must be configured before activation. LOCAL_PRESENCE is not
a step override.
Sending schedule
Read getWorkflow.sendWindow and change it with updateWorkflowSendWindow.
It applies to live runs at their next send, without republishing; a run
already waiting for the old schedule’s next opening keeps that wait and
checks again when it wakes. createWorkflow.sendWindow seeds it on creation.
updateWorkflowGraph cannot change it and refuses a graph carrying a
different sending schedule.
For sendWindow.timezoneMode, choose explicitly:
| Mode | Clock used |
|---|---|
BUSINESS |
The organization’s current timezone, falling back to New York when unset. |
RECIPIENT |
The text recipient’s phone-derived timezone; email and numbers without a derivable zone use the organization’s clock. |
CONFIGURED |
The window’s explicit timezone, independent of later organization changes. |
Send timezone as an IANA zone, such as America/New_York, with days
and optional timezoneMode. An omitted mode uses BUSINESS.
Each entry in the days array contains day: "MON" through "SUN" and
its own intervals of
{ start: { hour, minute }, end: { hour, minute } }. Both time components are
required. An absent day is closed; intervals cannot cross midnight, and
end: { hour: 0, minute: 0 } means midnight at the end of that day.
The older daysOfWeek/startTime/endTime shape is still accepted, but reads
return the per-day shape.
An unset sending schedule defaults to Monday–Friday, 09:00–19:00 Eastern.
The configured window is final for text and email; no extra lead-local quiet
hours are added. A window with no days or hours is refused; send null through
updateWorkflowSendWindow to restore the default.
Enrollment and exit behavior
config.triggerWindow controls enrollment and always uses its own
timezone. It accepts RECIPIENT or CONFIGURED as timezoneMode without
changing that behavior; BUSINESS is rejected there.
CONTACT exit conditions can also stop a run when a human, AI employee, or
another workflow sends to the contact: use outbound_from_human,
outbound_from_ai_employee, or outbound_from_other_workflow in
config.exitConditions.exitConditionTypes. A workflow’s own messages do
not trigger those exits.
deals_closed ends a run enrolled before the contact’s last live deal
reached a dead stage (terminal, or named like Dead / Lost / Not interested).
It leaves later win-back runs alone. A contact with no live deals does not
meet this condition.
Custom and system workflows
Every domain supports custom workflows, including PROPERTY and APPOINTMENT.
For a custom PROPERTY workflow, leave systemCategory null; it starts blank
and can run manually, on property signals, or from custom data pipelines.
To use the skip-trace starter instead, send
templateId: "property_signal_skip_trace" with
systemCategory: "PROPERTY_SIGNAL_SKIP_TRACE"; it carries the template’s
validation rules and daily skip-trace cap. Before promoting, configure a
MUTATION trigger whose filter names property signal types in a flat OR group
and a MATCH trigger with an AND root containing a top-level spicyLeadScore
minimum for the buy box. Read draftValidation for missing requirements.
Any non-null category requires a template id. Stored categories
APPOINTMENT_REMINDER and INTEGRATION_LEAD_HANDLING are not offered for creation. APPOINTMENT workflow
creation is available to members under their normal workflow permissions.
Integration lead-handling workflows are created
on integration connection; their steps cannot be added, removed, or rewired
through graph editing.
Skip-trace run budget (ADMIN scope)
| operationId | What it does |
|---|---|
setSkipTraceDailyCap |
Set the organization’s shared daily cap for all PROPERTY_SIGNAL_SKIP_TRACE workflows. dailyCap must be a whole number from 1 to 1,000,000,000. Read the current cap and usage with getWorkflowThrottleUsage; a raise reaches workflows already capped today within a few minutes. |
Each run spends a skip-trace credit, so raising the cap enables real spending
and requires a team admin’s ADMIN key. Confirm the old and new cap before
changing it.
Templates and task sets
The Content Templates API covers these operations in the workflows domain:
- Read with
getContentTemplate,listContentTemplates, andlistContentTemplateFolders(READ). Usetype: TASKortype: TASK_SETto find templates for the apply operations above. - Write with
createContentTemplate,updateContentTemplate, anddeleteContentTemplate(WRITE). Deleting a referenced template requires a dependency preview and replacement. - Organize with
moveContentTemplateToFolder,createContentTemplateFolder,renameContentTemplateFolder,moveContentTemplateFolder(nest a folder under another, orparentFolderId: nullfor the root), anddeleteContentTemplateFolder(WRITE).
applyTaskTemplate.overrides accepts title, description, endDate,
timezone, taskType, autoCompleteOnNoteAdded, and
autoCompleteOnReassignment. Send timezone with an endDate override;
otherwise the task retains the template’s timezone. Unspecified values and
supporting-file links still come from the template. For a task set, each
step’s own timing overrides the referenced template’s timing, and all offsets
resolve from one shared starting instant. See the Tasks API
to manage the resulting tasks.
Pausing and resuming (WRITE scope)
| operationId | What it does |
|---|---|
pauseWorkflow |
Pause the ACTIVE version — no new runs. pauseInFlightRuns: true also pauses runs already in flight; false lets them finish. AI-employee event routers cannot be paused. |
resumeWorkflow |
Resume the most-recently-paused version and wake runs paused by the workflow’s pause, continuing at their saved step. Runs paused by hand, a publication, failed send, or reply stay paused. If a version is already active, its workflow-paused runs are still woken. |
resumeWorkflowVersion |
Resume the most recently published version if it is PAUSED; pauses an active sibling first. A superseded version is refused. Restoring an older version as a draft and republishing it happens in the app’s version history. |
resumeRunsPausedWithWorkflow |
Wake the runs a workflow pause parked, on a workflow that is live again; their queued messages go out. It never turns the workflow itself on — resume or promote it first. Confirm before calling: this is a live send path. |
Run control (WRITE scope)
| operationId | What it does |
|---|---|
triggerWorkflowRun |
Manually start one run of the ACTIVE version against a record (contact/property/deal/task id per the workflow’s domain). |
enrollInWorkflow |
Bulk-enroll contacts into a CONTACT workflow’s ACTIVE version. Pass contactIds, or a saved filterId / inline filterTree with required selectCount: N. Async: returns mode and bulkTaskId; poll getBulkTask. See selection rules below. |
pauseWorkflowRun |
Pause one in-flight run (PENDING/RUNNING → PAUSED). |
resumeWorkflowRun |
Resume one paused run (PAUSED → PENDING). |
stopWorkflowRun |
Permanently stop one PENDING, RUNNING, or PAUSED run, cancelling queued sends. Already STOPPED is a no-op; COMPLETED, FAILED, and NOT_ENROLLED are refused. |
bulkPauseWorkflowRuns |
Pause many runs at once by explicit id (cap 500); ineligible ids are counted in skippedCount. |
bulkResumeWorkflowRuns |
Resume many paused runs at once by explicit id (cap 500). |
enrollInWorkflow accepts one selection form at a time, never ids combined
with a filter. A filter requires selectCount; there is no select-all.
excludeIds removes contacts from that first-N window, which uses default
filter order. Use countContacts to size the audience first. The maximum is
20,000 ids or selected contacts per call; split larger audiences with
exclusions or narrower filters. Confirm the workflow and count before
submitting: enrollment can start texts or emails under its sending rules.
For listWorkflowRuns, stoppedReason explains STOPPED and NOT_ENROLLED
outcomes; the latter never entered the workflow. PAUSED runs can carry
pausedReason and a carrier sendFailureCode. Only a PENDING run’s
queuedSend is current: reasons include DAILY_CAP, SEND_WINDOW,
WINDOW_LIMIT, and OUT_OF_SMS_CREDITS, with nextAttemptAt for its next
attempt. FAILED runs have no reason field, and older stopped runs can lack a
recorded reason. Report those gaps instead of inferring a cause.
Workflow Tables (READ + WRITE scope)
A Workflow Table is a lookup a run reads values from: each row is matched on a
key (keyKind such as COUNTY_FIPS, STATE, ZIP or FIELD_PATH, with
optional fallback levels), and each column has a tokenKey a message uses as a
merge field. A DEFAULT row answers when no key matches.
| operationId | Scope | What it does |
|---|---|---|
listWorkflowTables |
READ |
The organization’s tables (not archived), by name: key, fallback levels (keyShape), columns with their tokenKeys and SELECT options, and rowCount. No rows. Paged with limit (default 20) and offset; a short page is the last. |
getWorkflowTable |
READ |
One table with its rows, paged by rowLimit (default 100, at most 500) and rowOffset: each row’s key parts, its fallback keyLevel, whether it is the DEFAULT row, and its cells by columnId. Reading rows needs an org admin; returns null for a table outside your organization. |
listWorkflowTableChanges |
READ |
A table’s change history, newest first — who saved what and when. rowId narrows it to one row; page with after = nextCursor (at most 100 per page). |
previewWorkflowTableLookup |
READ |
Which row a run on one record would use, and each column’s value, resolved exactly as a workflow run resolves it (fallback levels, ranges, composite keys). Pass workflowTableId and one of contactId, propertyId, dealId or appointmentId. match: DEFAULT means the default row answered; NONE means nothing did. |
saveWorkflowTable |
WRITE |
Create a table (omit workflowTableId) or replace an existing table’s name, description and columns. The key is fixed at creation; fallback levels can be added later. |
saveWorkflowTableRows |
WRITE |
Create or replace rows (rows, each replaced whole, never merged) and delete rows (deleteRowIds) in one write. Pass expectedUpdatedAt from getWorkflowTable so a save over a newer edit is refused (CONFLICT). |
deleteWorkflowTable |
WRITE |
Archive a table. Refused while a workflow, message or form names one of its columns; the refusal lists them. |
Event workflow payload samples (READ + WRITE scope)
An EVENT workflow starts from a JSON payload your system posts. Save an
example payload first so its trigger fields can be mapped before anything
arrives.
| operationId | Scope | What it does |
|---|---|---|
listEventWorkflowPayloadSamples |
READ |
The example payloads an EVENT workflow has received or been given. Empty for other workflow domains. A sample whose id starts with run: came from a real run and cannot be deleted. |
recordEventWorkflowPayloadSample |
WRITE |
Save an example payload (a JSON object) on an EVENT workflow. Starts no run, sends nothing and leaves the graph alone. Refused on a workflow the key owner cannot edit. |
deleteEventWorkflowPayloadSample |
WRITE |
Permanently delete one saved example (sampleId from listEventWorkflowPayloadSamples). A run: sample returns NOT_FOUND. |
Content library files (READ + WRITE scope)
The files templates can attach (Automations → Templates → Files). Folders are
the content-template folders from listContentTemplateFolders.
| operationId | Scope | What it does |
|---|---|---|
listLibraryFiles |
READ |
The library’s files — id, name, description, folderId, contentType — optionally narrowed by folderId or a name search. |
addLibraryFileFromArtifact |
WRITE |
Add a file to the library by its artifactId, under a name, with an optional description and folderId. Templates can attach it once added. |
updateLibraryFile |
WRITE |
Rename a library file or change its description (null clears it). |
moveLibraryFile |
WRITE |
Move a file into a folder, or pass folderId: null for the root. |
deleteLibraryFile |
WRITE |
Take a file out of the library for good. The underlying upload stays, so templates that already attach it keep working. |
Folders (READ + WRITE scope)
Folders are a tree: each has an id, a name, a parentFolderId (null at
the root) and a displayOrder. Every folder operation addresses folders by id.
| operationId | Scope | What it does |
|---|---|---|
listWorkflowFolders |
READ |
List the organization’s workflow-folder tree — id, name, parentFolderId, displayOrder, itemCount. |
createWorkflowFolder |
WRITE |
Create an empty folder, optionally nested under parentFolderId; returns its id. |
moveWorkflowToFolder |
WRITE |
Move a workflow (by its group id) into a folder by folderId (null to ungroup). |
moveWorkflowFolder |
WRITE |
Move a folder under another folder, or pass parentFolderId: null to move it to the root. |
renameWorkflowFolder |
WRITE |
Rename a folder by folderId. |
deleteWorkflowFolder |
WRITE |
Delete a folder by folderId; its direct workflows and child folders move to its parent, or to the root if it had no parent. Contents are not deleted. |
reorderWorkflowFolders |
WRITE |
Persist a new display order for the folders that share one parent — pass parentFolderId (null for the root) and the ordered folderIds. |
Example
Trigger the live version of a workflow against one contact:
curl -X POST https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer gsk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "operationId": "triggerWorkflowRun", "variables": { "workflowGroupId": "GROUP_ID", "recordId": "CONTACT_ID" } }'
Full variable schemas, response shapes, and worked examples for every operation are available from the discovery endpoint.