saveWorkflowTable
Create a Workflow Table.
Create a Workflow Table (workflowTableId omitted) or replace an existing one's name, description and columns. The key (keyKind, keyFieldPath) is fixed at creation; fallback levels may be added later. A column left out of an update is DELETED, with its value in every row: permanent, so this needs the person's approval. Rows are written with saveWorkflowTableRows. Returns the table's shape.
curl https://server.goliathdata.com/api/v1/call \
-H "Authorization: Bearer $GOLIATH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"operationId": "saveWorkflowTable",
"variables": {
"input": {
"name": "<String>",
"keyKind": "COUNTY_FIPS",
"columns": [
{
"name": "<String>",
"kind": "TEXT"
}
]
}
}
}'import { GoliathClient } from '@goliath-data/sdk'
const goliath = new GoliathClient({
apiKey: process.env.GOLIATH_API_KEY!,
baseUrl: 'https://server.goliathdata.com',
})
// Enum values as strings: the SDK's enum types aren't exported yet, so assert to the method's variables type.
const variables = {
input: {
name: '<String>',
keyKind: 'COUNTY_FIPS',
columns: [
{
name: '<String>',
kind: 'TEXT'
}
]
}
} as Parameters<typeof goliath.workflows.saveWorkflowTable>[0]
const result = await goliath.workflows.saveWorkflowTable(variables, { idempotencyKey: crypto.randomUUID() })const res = await fetch('https://server.goliathdata.com/api/v1/call', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GOLIATH_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
operationId: 'saveWorkflowTable',
variables: {
input: {
name: '<String>',
keyKind: 'COUNTY_FIPS',
columns: [
{
name: '<String>',
kind: 'TEXT'
}
]
}
}
}),
})
const { data, errors } = await res.json()Variables
The example sends only what the schema requires. Some operations also need one of several optional variables (one filter or another, say); the description above says which.
Creates a table when `workflowTableId` is null, otherwise replaces that table's name, description and COLUMN SET. Rows are saved separately (see `saveWorkflowTableRows`) because the editor saves a grid of rows at once and should not have to resend every row to rename a column.
Null creates a new table.
Fixed at creation and ignored on update: re-keying a table would silently invalidate every row's key, and the honest way to do that is a new table. COUNTY_FIPS: A 5-digit county FIPS code, matched against the property's `countyFips`. That is the value `countyFilters` compiles to, so a row matching here is one the same market's saved filter also matches. A display name is never a key. STATE: The property's 2-letter state code, e.g. `FL`. CITY: The property's city, by the `City` record's id: never a typed name. ZIP_CODE: The property's 5-digit ZIP code. PROPERTY_TYPE: The property's `PropertyTy…[truncated: +1389 chars]
Required when `keyKind` is `FIELD_PATH`: the BUILT-IN contact, deal or appointment variable rows are keyed on, such as `contactPropertyCity` or `dealTitle`. A custom field, a property variable and the refused deal and appointment variables are rejected: see `WorkflowTable.keyFieldPath`. Ignored for every other kind, and ignored on update like `keyKind`.
The fallback chain, composite and ranged keys: see `WorkflowTable.keyShape`. Optional: absent means the single exact key `keyKind` / `keyFieldPath` name. On create, its first part must be that same key. On update it may ADD fallback levels, and change or remove a fallback level that holds no rows; a change to the first level, or to a level that has rows, is refused, because it would reinterpret every one of those rows' keys at once. Absent on update leaves the shape as it is.
The complete column set, in the order given. A column absent from this list is DELETED along with its values on every row: and a delete is refused outright while a workflow step or a message body still names that column, unless `dependencyResolution` carries a replacement for it.
How to repoint everything that names a column this save deletes. Required only when the save actually removes a column that has dependents; the deletion impact dialog is what produces it.
Optimistic-concurrency token: the table's `updatedAt` as the editor loaded it. When provided and the row has changed since, the save is refused with CONFLICT rather than replacing whatever the other editor committed. Omit only on create, where there is nothing to conflict with.
Every field, all the way down
[
{
"name": "input",
"type": "SaveWorkflowTableInput!",
"required": true,
"description": "Creates a table when `workflowTableId` is null, otherwise replaces that table's name, description and COLUMN SET. Rows are saved separately (see `saveWorkflowTableRows`) because the editor saves a grid of rows at once and should not have to resend every row to rename a column.",
"defaultValue": null,
"fields": [
{
"name": "workflowTableId",
"type": "ID",
"required": false,
"description": "Null creates a new table.",
"defaultValue": null
},
{
"name": "name",
"type": "String!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "description",
"type": "String",
"required": false,
"description": null,
"defaultValue": null
},
{
"name": "keyKind",
"type": "WorkflowTableKeyKind!",
"required": true,
"description": "Fixed at creation and ignored on update: re-keying a table would silently invalidate every row's key, and the honest way to do that is a new table. COUNTY_FIPS: A 5-digit county FIPS code, matched against the property's `countyFips`. That is the value `countyFilters` compiles to, so a row matching here is one the same market's saved filter also matches. A display name is never a key. STATE: The property's 2-letter state code, e.g. `FL`. CITY: The property's city, by the `City` record's id: never a typed name. ZIP_CODE: The property's 5-digit ZIP code. PROPERTY_TYPE: The property's `PropertyTy…[truncated: +1389 chars]",
"defaultValue": null,
"enumValues": [
"COUNTY_FIPS",
"STATE",
"CITY",
"ZIP_CODE",
"PROPERTY_TYPE",
"OCCUPANCY_TYPE",
"OWNER_TYPE",
"OWNER_LOCATION",
"MLS_STATUS",
"FIELD_PATH",
"CONTACT_LIST",
"CONTACT_TAG",
"CONTACT_AGENT",
"CONTACT_CONTRIBUTOR",
"PIPELINE",
"DEAL_PIPELINE",
"PIPELINE_STAGE",
"DEAL_STAGE",
"PIPELINE_ITEM_ASSIGNEE",
"DEAL_ASSIGNEE",
"APPOINTMENT_ATTENDEE"
]
},
{
"name": "keyFieldPath",
"type": "String",
"required": false,
"description": "Required when `keyKind` is `FIELD_PATH`: the BUILT-IN contact, deal or appointment variable rows are keyed on, such as `contactPropertyCity` or `dealTitle`. A custom field, a property variable and the refused deal and appointment variables are rejected: see `WorkflowTable.keyFieldPath`. Ignored for every other kind, and ignored on update like `keyKind`.",
"defaultValue": null
},
{
"name": "keyShape",
"type": "WorkflowTableKeyShapeInput",
"required": false,
"description": "The fallback chain, composite and ranged keys: see `WorkflowTable.keyShape`. Optional: absent means the single exact key `keyKind` / `keyFieldPath` name. On create, its first part must be that same key. On update it may ADD fallback levels, and change or remove a fallback level that holds no rows; a change to the first level, or to a level that has rows, is refused, because it would reinterpret every one of those rows' keys at once. Absent on update leaves the shape as it is.",
"defaultValue": null,
"fields": [
{
"name": "levels",
"type": "[WorkflowTableKeyLevelInput!]!",
"required": true,
"description": null,
"defaultValue": null,
"fields": [
{
"name": "parts",
"type": "[WorkflowTableKeyPartInput!]!",
"required": true,
"description": null,
"defaultValue": null,
"fields": [
{
"name": "kind",
"type": "WorkflowTableKeyKind!",
"required": true,
"description": "What a Workflow Table's key column is keyed ON: the field of the subject each row is matched against. One kind per table, fixed when the table is created. COUNTY_FIPS: A 5-digit county FIPS code, matched against the property's `countyFips`. That is the value `countyFilters` compiles to, so a row matching here is one the same market's saved filter also matches. A display name is never a key. STATE: The property's 2-letter state code, e.g. `FL`. CITY: The property's city, by the `City` record's id: never a typed name. ZIP_CODE: The property's 5-digit ZIP code. PROPERTY_TYPE: The property's `Pr…[truncated: +1397 chars]",
"defaultValue": null,
"enumValues": [
"COUNTY_FIPS",
"STATE",
"CITY",
"ZIP_CODE",
"PROPERTY_TYPE",
"OCCUPANCY_TYPE",
"OWNER_TYPE",
"OWNER_LOCATION",
"MLS_STATUS",
"FIELD_PATH",
"CONTACT_LIST",
"CONTACT_TAG",
"CONTACT_AGENT",
"CONTACT_CONTRIBUTOR",
"PIPELINE",
"DEAL_PIPELINE",
"PIPELINE_STAGE",
"DEAL_STAGE",
"PIPELINE_ITEM_ASSIGNEE",
"DEAL_ASSIGNEE",
"APPOINTMENT_ATTENDEE"
]
},
{
"name": "fieldPath",
"type": "String",
"required": false,
"description": "Required for a `FIELD_PATH` part, refused on every other kind.",
"defaultValue": null
},
{
"name": "match",
"type": "WorkflowTableKeyMatch!",
"required": true,
"description": "How one part of a Workflow Table's key is compared with the subject's value. EXACT: Equal, in the key kind's canonical spelling. RANGE: Inside the row's inclusive range. Only offered for kinds whose values have an order: a ZIP code, for one.",
"defaultValue": null,
"enumValues": [
"EXACT",
"RANGE"
]
}
]
}
]
}
]
},
{
"name": "columns",
"type": "[SaveWorkflowTableColumnInput!]!",
"required": true,
"description": "The complete column set, in the order given. A column absent from this list is DELETED along with its values on every row: and a delete is refused outright while a workflow step or a message body still names that column, unless `dependencyResolution` carries a replacement for it.",
"defaultValue": null,
"fields": [
{
"name": "id",
"type": "ID",
"required": false,
"description": "Null creates a new column. An unknown id is rejected.",
"defaultValue": null
},
{
"name": "name",
"type": "String!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "kind",
"type": "WorkflowTableColumnKind!",
"required": true,
"description": "Ignored on an existing column. A column's kind is what its stored values mean, so changing it would reinterpret every row at once. TEXT: A literal string, rendered into a message body as typed. LONG_TEXT: A literal block of copy. Plain text: it carries no merge fields of its own. NUMBER: A number, stored canonically and rendered grouped (`1,234.5`). CURRENCY: A USD amount, stored as a decimal string and rendered to the cent (`$1,200.00`). PERCENT: A percentage in percent units: `12.5` means 12.5%, rendered `12.5%`. BOOLEAN: A yes/no, stored `true`/`false` and rendered `Yes`/`No`. DURATION: A…[truncated: +1408 chars]",
"defaultValue": null,
"enumValues": [
"TEXT",
"LONG_TEXT",
"NUMBER",
"CURRENCY",
"PERCENT",
"BOOLEAN",
"DURATION",
"SELECT",
"DATE",
"TIME_OF_DAY",
"TIMEZONE",
"URL",
"EMAIL",
"PHONE",
"CONTACT_LIST",
"PROPERTY_LIST",
"WORKFLOW_GROUP",
"USER",
"PHONE_LINE",
"TEXT_TEMPLATE",
"EMAIL_TEMPLATE",
"CONTACT_TAG",
"USER_GROUP",
"AI_EMPLOYEE",
"PIPELINE_STAGE",
"DEAL_STAGE"
]
},
{
"name": "options",
"type": "[WorkflowTableColumnOptionInput!]",
"required": false,
"description": "The complete choice set for a `SELECT` column, in the order to present it. Required there and refused on every other kind. Unlike `kind`, this IS editable after the column exists: adding a choice, or renaming one, reinterprets nothing: a cell stores the choice's `value`, and a `value` that disappears leaves its cells readable and merely unpickable.",
"defaultValue": null,
"fields": [
{
"name": "value",
"type": "String!",
"required": true,
"description": "What a cell stores. Stable; renaming the label never changes it.",
"defaultValue": null
},
{
"name": "label",
"type": "String!",
"required": true,
"description": "What a person reads, in the grid and in a rendered message.",
"defaultValue": null
}
]
}
]
},
{
"name": "dependencyResolution",
"type": "DependencyResolutionInput",
"required": false,
"description": "How to repoint everything that names a column this save deletes. Required only when the save actually removes a column that has dependents; the deletion impact dialog is what produces it.",
"defaultValue": null,
"fields": [
{
"name": "impactVersion",
"type": "String!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "replacements",
"type": "[DependencyReplacementSelectionInput!]!",
"required": true,
"description": null,
"defaultValue": null,
"fields": [
{
"name": "targetKind",
"type": "DependencyTargetKind!",
"required": true,
"description": null,
"defaultValue": null,
"enumValues": [
"PIPELINE_ITEM",
"DEAL",
"PIPELINE_STAGE_PIPELINE_ITEMS",
"DEAL_STAGE_DEALS",
"PIPELINE_ITEM_CUSTOM_FIELD",
"DEAL_CUSTOM_FIELD",
"PIPELINE_ITEM_CUSTOM_FIELD_OPTION",
"DEAL_CUSTOM_FIELD_OPTION",
"PROPERTY_TAG",
"PROPERTY_LIST",
"CONTACT_TAG",
"CONTACT_LIST",
"CONTACT_CUSTOM_FIELD",
"CONTACT_CUSTOM_FIELD_OPTION",
"PIPELINE",
"DEAL_PIPELINE",
"PIPELINE_STAGE",
"DEAL_STAGE",
"TWILIO_PHONE_NUMBER",
"EMAIL_IDENTITY",
"LIBRARY_FOLDER",
"USER",
"USER_GROUP",
"WORKFLOW_GROUP",
"WORKFLOW_AUTOMATION",
"CONTENT_TEMPLATE",
"AGENT",
"MARKETING_CAMPAIGN",
"UPLOADED_FILE",
"WORKFLOW_WEBHOOK_CREDENTIAL",
"SCRAPER_PIPELINE",
"FILTER",
"EMBED_FORM",
"WORKFLOW_TABLE",
"WORKFLOW_TABLE_COLUMN",
"CONTACT",
"PROPERTY",
"ORGANIZATION",
"SLACK_CHANNEL",
"SLACK_USER",
"MACHINE_GROUP",
"MACHINE_HOST"
]
},
{
"name": "targetId",
"type": "ID!",
"required": true,
"description": null,
"defaultValue": null
},
{
"name": "replacementId",
"type": "ID!",
"required": true,
"description": null,
"defaultValue": null
}
]
}
]
},
{
"name": "expectedUpdatedAt",
"type": "DateTime",
"required": false,
"description": "Optimistic-concurrency token: the table's `updatedAt` as the editor loaded it. When provided and the row has changed since, the save is refused with CONFLICT rather than replacing whatever the other editor committed. Omit only on create, where there is nothing to conflict with.",
"defaultValue": null
}
]
}
]Response
What comes back under data: a list shows as one entry, and a value as its type.
{
"data": {
"workflowAutomationsMutation": {
"saveWorkflowTable": {
"id": "<ID>",
"name": "…",
"description": "…",
"keyKind": "COUNTY_FIPS",
"keyFieldPath": "…",
"keyShape": {
"levels": [
{
"parts": [
{
"kind": "COUNTY_FIPS",
"fieldPath": "…",
"match": "EXACT"
}
]
}
]
},
"rowCount": 0,
"isProvisioned": true,
"updatedAt": "2026-01-01T00:00:00.000Z",
"columns": [
{
"id": "<ID>",
"name": "…",
"kind": "TEXT",
"displayOrder": 0,
"tokenKey": "…",
"options": [
{
"value": "…",
"label": "…"
}
]
}
]
}
}
}
}Errors
unknown_variableA variable the operation does not declare. A typo is refused rather than silently dropped.
unauthorizedNo bearer header, or the key is invalid or revoked.
insufficient_scopeThe key lacks the scope the operation requires.
forbiddenA referenced record does not exist, was deleted, or is outside your organization. The gateway cannot tell those apart.
idempotency_conflictA request with this Idempotency-Key is still in flight.
Idempotency
Send an Idempotency-Key of 1 to 255 characters. A finished request retried with the same key replays its stored response, marked Idempotency-Replayed: true; one still in flight returns 409 idempotency_conflict. Keys expire after 24 hours. Write without fear →