Skip to content

Workflows

This content is not available in your language yet.

A workflow is the graph of nodes an agent runs on every turn. Building one takes three calls with a dashboard JWT: createWorkflow (name only), updateWorkflow (the nodes and edges, sent as variables) and publishWorkflow. Then attach it to an agent with updateAgent. Node type values inside the graph are lowercase (start, llm, response); the NodeType enum in this API is the same list in capitals. Every node type and its fields are documented under Workflows, Node Types. Every publish that changes the graph is kept as a version you can list, compare and roll back to. With an API key you can also run a published workflow without a chat (runWorkflowTask) and read the outcome (workflowTaskOutcome).

Every operation is a POST of { "query", "variables" } to https://api.wetel.dev/graphql (sandbox) with the x-huat-platform: customer header and the credential shown. New here? Start with the Quickstart and the Authentication cheat sheet. Errors are listed in Errors; Postman and OpenAPI files are on Downloads.

OperationKindCredentialWhat it does
createWorkflowmutationDashboard JWTCreate an empty workflow (name and description only).
updateWorkflowmutationDashboard JWTSave the workflow’s draft graph (nodes and edges) and optionally its name.
workflowDiagnosticsqueryDashboard JWTLint a workflow graph. Lists findings that will probably misbehave at run time; never blocks.
publishWorkflowmutationDashboard JWTPublish the current draft. New turns of every session on the agent run this version.
workflowVersionsqueryDashboard JWTList a workflow’s published versions, newest first (summaries only, no graph).
workflowVersionDiffqueryDashboard JWTWhat changed between two versions, or between a version and the current draft. Ids and key names only, never values.
rollbackWorkflowmutationDashboard JWTPublish an earlier version’s graph again, as a new version. The draft is not changed.
restoreWorkflowVersionToDraftmutationDashboard JWTCopy a version’s graph into the draft, replacing it. Nothing is published.
nodeConfigSchemasqueryAPI keyThe JSON Schema of every node type’s config, for building or validating graphs in your own tools.
workflowRunsqueryDashboard JWTList the runs of one session, newest first, to debug what a workflow did.
workflowRunqueryDashboard JWTRead one run’s node trace and context, to see exactly which nodes ran, in order, and why.
pendingAwaitRepliesqueryDashboard JWTList the conversations waiting at an await_reply node for the user’s answer, newest first.
runWorkflowTaskmutationAPI keyRun an agent’s published workflow headlessly (no chat message). Asynchronous; returns once accepted.
workflowTaskOutcomequeryAPI keyRead the outcome of a task started with runWorkflowTask, with the same API key.

Create an empty workflow (name and description only).

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • CreateWorkflowInput has no nodes or edges: add the graph with updateWorkflow.

Request

mutation CreateWorkflow($input: CreateWorkflowInput!) {
createWorkflow(input: $input) {
id
name
version
isPublished
}
}
variables
{
"input": {
"name": "Ticket routing",
"description": "Classifies a ticket as L1, L2 or L3 and tells the user where it goes."
}
}

Input fields

CreateWorkflowInput

FieldTypeRequiredDescription
descriptionStringnoOptional free-text description (at most 500 characters).
nameString!yesDisplay name (at most 120 characters).

Code samples

createWorkflow.sh
#!/usr/bin/env bash
# createWorkflow: Create an empty workflow (name and description only).
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation CreateWorkflow($input:CreateWorkflowInput!){createWorkflow(input:$input){id name version isPublished}}",
"variables": {
"input": {
"name": "Ticket routing",
"description": "Classifies a ticket as L1, L2 or L3 and tells the user where it goes."
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"createWorkflow": {
"id": 12,
"name": "Ticket routing",
"version": 1,
"isPublished": false
}
}
}
UNAUTHENTICATED: Called with an API key instead of a JWT (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createWorkflow"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}

Save the workflow’s draft graph (nodes and edges) and optionally its name.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • nodes and edges are JSON: always send them through variables, never inline in the query.
  • The draft is what you edit; sessions keep running the last published version until you call publishWorkflow.
  • A node config the platform cannot accept (a missing field, an out-of-range number, an unknown value) is refused with BAD_USER_INPUT and a message naming the node and the field. A draft may still be structurally incomplete; publishWorkflow checks the whole graph.
  • input.settings.failureResponse sets plain text (1 to 500 characters) sent instead of the standard error reply when a run fails; null clears it. Effective on save, not on publish.

Request

mutation SaveGraph($input: UpdateWorkflowInput!) {
updateWorkflow(input: $input) {
id
version
isPublished
updatedAt
}
}
variables
{
"input": {
"id": 12,
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": {
"x": 0,
"y": 0
}
},
{
"id": "classify",
"type": "llm",
"label": "Classify level",
"config": {
"promptTemplate": "You route IT support tickets. Read the ticket and answer with one word: L1 for password resets, account unlocks and how-to questions; L2 for a broken device, application or connection that needs a technician; L3 for an outage, a security incident or anything affecting many people.\n\nTicket: \"{{userMessage}}\"",
"outputVar": "level",
"matchAnyOf": [
"L1",
"L2",
"L3"
],
"streamOutput": false
},
"position": {
"x": 0,
"y": 100
}
},
{
"id": "reply",
"type": "response",
"label": "Tell the user",
"config": {
"messageTemplate": "Thanks. Your ticket goes to the {{level}} queue; we will reply here."
},
"position": {
"x": 0,
"y": 200
}
}
],
"edges": [
{
"id": "e1",
"source": "start",
"target": "classify"
},
{
"id": "e2",
"source": "classify",
"target": "reply"
}
]
}
}

Input fields

UpdateWorkflowInput

FieldTypeRequiredDescription
descriptionStringnoNew description (at most 500 characters).
edgesJSONnoThe complete edge list of the draft, replacing the stored one: a JSON array (send it through variables) of {id, source, target, label?}, at most 400 edges. source and target are node ids; label selects the branch for routing nodes (for example “true” and “false” after a condition node).
idInt!yesInteger id of the workflow to change.
nameStringnoNew display name (at most 120 characters).
nodesJSONnoThe complete node list of the draft, replacing the stored one: a JSON array (send it through variables) of {id, type, label, config, position: {x, y}}, at most 200 nodes. type is a NodeType value in lower case (for example “llm”); config follows that type’s schema from nodeConfigSchemas and is validated on save.
settingsWorkflowSettingsInputnoWorkflow-level settings to change, merged into the stored ones (an omitted field inside keeps its value; omitting settings, or sending null, changes nothing). Effective on save, not on publish.

WorkflowSettingsInput

FieldTypeRequiredDescription
failureResponseStringnoPlain text (1 to 500 characters after trimming) sent instead of the standard error reply when a run fails. Refused (BAD_USER_INPUT) when it contains {{ }} templates, HTML tags, control characters or anything shaped like a credential or signed URL. null clears it (back to the standard reply); omit it to keep the stored value.
notifyOnFailureWorkflowFailureNotifynoOWNERS turns on the in-app failure notice to the workspace owners; NONE (or null) turns it off. Omit it to keep the stored value. Effective on save, no publish needed.

Allowed values

  • WorkflowFailureNotify: NONE, OWNERS

Code samples

updateWorkflow.sh
#!/usr/bin/env bash
# updateWorkflow: Save the workflow's draft graph (nodes and edges) and optionally its name.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation SaveGraph($input:UpdateWorkflowInput!){updateWorkflow(input:$input){id version isPublished updatedAt}}",
"variables": {
"input": {
"id": 12,
"nodes": [
{
"id": "start",
"type": "start",
"label": "Start",
"config": {},
"position": {
"x": 0,
"y": 0
}
},
{
"id": "classify",
"type": "llm",
"label": "Classify level",
"config": {
"promptTemplate": "You route IT support tickets. Read the ticket and answer with one word: L1 for password resets, account unlocks and how-to questions; L2 for a broken device, application or connection that needs a technician; L3 for an outage, a security incident or anything affecting many people.\n\nTicket: \"{{userMessage}}\"",
"outputVar": "level",
"matchAnyOf": [
"L1",
"L2",
"L3"
],
"streamOutput": false
},
"position": {
"x": 0,
"y": 100
}
},
{
"id": "reply",
"type": "response",
"label": "Tell the user",
"config": {
"messageTemplate": "Thanks. Your ticket goes to the {{level}} queue; we will reply here."
},
"position": {
"x": 0,
"y": 200
}
}
],
"edges": [
{
"id": "e1",
"source": "start",
"target": "classify"
},
{
"id": "e2",
"source": "classify",
"target": "reply"
}
]
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"updateWorkflow": {
"id": 12,
"version": 1,
"isPublished": false,
"updatedAt": "2026-10-12T04:05:41.020Z"
}
}
}
NOT_FOUND: Workflow not in your workspace (HTTP 200)
{
"errors": [
{
"message": "Workflow 12 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"updateWorkflow"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Workflow 12 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}
BAD_USER_INPUT: name sent as a number (HTTP 400)

Sent with variables:

{
"input": {
"id": 12,
"name": 42
}
}
{
"errors": [
{
"message": "Variable \"$input\" got invalid value 42 at \"input.name\"; String cannot represent a non string value: 42",
"locations": [
{
"line": 1,
"column": 20
}
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
]
}

Lint a workflow graph. Lists findings that will probably misbehave at run time; never blocks.

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • published: false checks the draft (what updateWorkflow wrote); published: true checks the version sessions run.
  • Switch on code, not on message. An empty list means nothing was found. Publishing has its own blocking checks.

Request

query Lint($id: Int!, $published: Boolean!) {
workflowDiagnostics(id: $id, published: $published) {
code
severity
nodeId
edgeId
message
}
}
variables
{
"id": 12,
"published": false
}

Allowed values

  • WorkflowDiagnosticSeverity: INFO, WARNING

Code samples

workflowDiagnostics.sh
#!/usr/bin/env bash
# workflowDiagnostics: Lint a workflow graph. Lists findings that will probably misbehave at run time; never blocks.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query Lint($id:Int!$published:Boolean!){workflowDiagnostics(id:$id published:$published){code severity nodeId edgeId message}}",
"variables": {
"id": 12,
"published": false
}
}
JSON

Responses

One finding (HTTP 200)

{
"data": {
"workflowDiagnostics": [
{
"code": "UNREACHABLE_NODE",
"severity": "WARNING",
"nodeId": "notes",
"edgeId": null,
"message": "Node \"notes\" (\"Internal notes\") cannot be reached from the start node, so it never runs, and publish will refuse the workflow. Connect it to the graph or delete it."
}
]
}
}

Nothing found (HTTP 200)

{
"data": {
"workflowDiagnostics": []
}
}
NOT_FOUND: Never published, asked for the published version (HTTP 200)
{
"errors": [
{
"message": "Workflow 12 has no published version; omit published to check the draft",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowDiagnostics"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Workflow 12 has no published version; omit published to check the draft",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}

Publish the current draft. New turns of every session on the agent run this version.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Refused when the graph is not runnable, for example with no start node or with a node that cannot be reached.

Request

mutation Publish($id: Int!) {
publishWorkflow(id: $id) {
id
version
isPublished
}
}
variables
{
"id": 12
}

Code samples

publishWorkflow.sh
#!/usr/bin/env bash
# publishWorkflow: Publish the current draft. New turns of every session on the agent run this version.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation Publish($id:Int!){publishWorkflow(id:$id){id version isPublished}}",
"variables": {
"id": 12
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"publishWorkflow": {
"id": 12,
"version": 2,
"isPublished": true
}
}
}
BAD_USER_INPUT: Node type written in capitals (HTTP 200)
{
"errors": [
{
"message": "node classify: type \"LLM\" must be lowercase: \"llm\"",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"publishWorkflow"
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
],
"data": null
}
BAD_REQUEST: No start node (HTTP 200)
{
"errors": [
{
"message": "Workflow must have a START node before publishing (node types seen: llm, response; the start type is \"start\", lowercase)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"publishWorkflow"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "Workflow must have a START node before publishing (node types seen: llm, response; the start type is \"start\", lowercase)",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}

List a workflow’s published versions, newest first (summaries only, no graph).

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • One entry for every publish, rollback or template hire that changed the published graph; republishing an unchanged graph adds none.
  • versionNumber (1, 2, 3 …) is what the other version operations take. id is the value currentVersionId and a run’s workflowVersionId hold. isCurrent marks the version new turns run now.
  • Page with first (default 20, at most 100) and after (a previous page’s pageInfo.endCursor, older versions). The newest 100 versions are kept, so numbers can have gaps.

Request

query History($workflowId: Int!, $first: Int, $after: String) {
workflowVersions(workflowId: $workflowId, first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
versionNumber
isCurrent
source
restoredFromVersionNumber
note
createdAt
}
}
}
}
variables
{
"workflowId": 12,
"first": 20
}

Allowed values

  • WorkflowVersionSource: BACKFILL, HIRE, PUBLISH, ROLLBACK, SEED

Code samples

workflowVersions.sh
#!/usr/bin/env bash
# workflowVersions: List a workflow's published versions, newest first (summaries only, no graph).
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query History($workflowId:Int!$first:Int$after:String){workflowVersions(workflowId:$workflowId first:$first after:$after){pageInfo{hasNextPage endCursor}edges{node{id versionNumber isCurrent source restoredFromVersionNumber note createdAt}}}}",
"variables": {
"workflowId": 12,
"first": 20
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"workflowVersions": {
"pageInfo": {
"hasNextPage": false,
"endCursor": "djox"
},
"edges": [
{
"node": {
"id": 305,
"versionNumber": 2,
"isCurrent": true,
"source": "PUBLISH",
"restoredFromVersionNumber": null,
"note": "Stricter classifier prompt",
"createdAt": "2026-10-15T08:40:10.000Z"
}
},
{
"node": {
"id": 291,
"versionNumber": 1,
"isCurrent": false,
"source": "PUBLISH",
"restoredFromVersionNumber": null,
"note": null,
"createdAt": "2026-10-14T09:02:31.000Z"
}
}
]
}
}
}
NOT_FOUND: Workflow not in your workspace (HTTP 200)
{
"errors": [
{
"message": "Workflow 12 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowVersions"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Workflow 12 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}
BAD_REQUEST: Cursor not issued by this query (HTTP 200)
{
"errors": [
{
"message": "Invalid `after` cursor",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowVersions"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "Invalid `after` cursor",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}

What changed between two versions, or between a version and the current draft. Ids and key names only, never values.

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Omit toVersion to compare against the draft. With fromVersion set to the current version, that is exactly what the next publishWorkflow would change.
  • changedKeys names top-level keys (label, position, type) and config keys as config.<key>. Read both graphs with workflowVersion to see the values.

Request

query Diff($workflowId: Int!, $fromVersion: Int!, $toVersion: Int) {
workflowVersionDiff(workflowId: $workflowId, fromVersion: $fromVersion, toVersion: $toVersion) {
addedNodeIds
removedNodeIds
changedNodes {
id
type
changedKeys
}
addedEdgeIds
removedEdgeIds
changedEdgeIds
}
}
variables
{
"workflowId": 12,
"fromVersion": 2
}

Code samples

workflowVersionDiff.sh
#!/usr/bin/env bash
# workflowVersionDiff: What changed between two versions, or between a version and the current draft. Ids and key names only, never values.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query Diff($workflowId:Int!$fromVersion:Int!$toVersion:Int){workflowVersionDiff(workflowId:$workflowId fromVersion:$fromVersion toVersion:$toVersion){addedNodeIds removedNodeIds changedNodes{id type changedKeys}addedEdgeIds removedEdgeIds changedEdgeIds}}",
"variables": {
"workflowId": 12,
"fromVersion": 2
}
}
JSON

Responses

Draft compared with version 2 (HTTP 200)

{
"data": {
"workflowVersionDiff": {
"addedNodeIds": [],
"removedNodeIds": [],
"changedNodes": [
{
"id": "classify",
"type": "llm",
"changedKeys": [
"config.promptTemplate"
]
}
],
"addedEdgeIds": [],
"removedEdgeIds": [],
"changedEdgeIds": []
}
}
}
NOT_FOUND: Version does not exist or was pruned (HTTP 200)
{
"errors": [
{
"message": "Version 2 of workflow 12 not found (it may have been pruned)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowVersionDiff"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Version 2 of workflow 12 not found (it may have been pruned)",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}

Publish an earlier version’s graph again, as a new version. The draft is not changed.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • The old graph is checked against today’s rules, as a publish is, and refused with the same errors as publishWorkflow when it no longer passes.
  • Refused when that graph is already the one published now. note is optional, at most 500 characters.
  • Takes effect for the next session turn; a conversation paused at await_reply or approval keeps the version it paused on.

Request

mutation Rollback($workflowId: Int!, $versionNumber: Int!, $note: String) {
rollbackWorkflow(workflowId: $workflowId, versionNumber: $versionNumber, note: $note) {
id
currentVersionId
isPublished
}
}
variables
{
"workflowId": 12,
"versionNumber": 1,
"note": "Back to the first prompt while the classifier is fixed"
}

Code samples

rollbackWorkflow.sh
#!/usr/bin/env bash
# rollbackWorkflow: Publish an earlier version's graph again, as a new version. The draft is not changed.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation Rollback($workflowId:Int!$versionNumber:Int!$note:String){rollbackWorkflow(workflowId:$workflowId versionNumber:$versionNumber note:$note){id currentVersionId isPublished}}",
"variables": {
"workflowId": 12,
"versionNumber": 1,
"note": "Back to the first prompt while the classifier is fixed"
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"rollbackWorkflow": {
"id": 12,
"currentVersionId": 318,
"isPublished": true
}
}
}
BAD_USER_INPUT: That graph is already published (HTTP 200)
{
"errors": [
{
"message": "Version 1 has the same graph as the version published now; nothing to roll back.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"rollbackWorkflow"
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
],
"data": null
}
NOT_FOUND: Version does not exist or was pruned (HTTP 200)
{
"errors": [
{
"message": "Version 1 of workflow 12 not found (it may have been pruned)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"rollbackWorkflow"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Version 1 of workflow 12 not found (it may have been pruned)",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}

Copy a version’s graph into the draft, replacing it. Nothing is published.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Unpublished edits in the draft are replaced. Edit the restored draft, then call publishWorkflow.
  • The graph is checked like an updateWorkflow save, so a node config that no longer passes today’s rules is refused with BAD_USER_INPUT.

Request

mutation RestoreToDraft($workflowId: Int!, $versionNumber: Int!) {
restoreWorkflowVersionToDraft(workflowId: $workflowId, versionNumber: $versionNumber) {
id
version
updatedAt
}
}
variables
{
"workflowId": 12,
"versionNumber": 1
}

Code samples

restoreWorkflowVersionToDraft.sh
#!/usr/bin/env bash
# restoreWorkflowVersionToDraft: Copy a version's graph into the draft, replacing it. Nothing is published.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation RestoreToDraft($workflowId:Int!$versionNumber:Int!){restoreWorkflowVersionToDraft(workflowId:$workflowId versionNumber:$versionNumber){id version updatedAt}}",
"variables": {
"workflowId": 12,
"versionNumber": 1
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"restoreWorkflowVersionToDraft": {
"id": 12,
"version": 5,
"updatedAt": "2026-10-16T03:20:11.400Z"
}
}
}
NOT_FOUND: Version does not exist or was pruned (HTTP 200)
{
"errors": [
{
"message": "Version 1 of workflow 12 not found (it may have been pruned)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"restoreWorkflowVersionToDraft"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Version 1 of workflow 12 not found (it may have been pruned)",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}
BAD_USER_INPUT: versionNumber sent as a string (HTTP 400)

Sent with variables:

{
"workflowId": 12,
"versionNumber": "one"
}
{
"errors": [
{
"message": "Variable \"$versionNumber\" got invalid value \"one\"; Int cannot represent non-integer value: \"one\"",
"locations": [
{
"line": 1,
"column": 44
}
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
]
}

The JSON Schema of every node type’s config, for building or validating graphs in your own tools.

  • Kind: query
  • Credential: API key: X-Api-Key: sk_..., kept on your server only
  • Available in: sandbox and production
  • Accepts an API key or a dashboard JWT. The answer is the same for every workspace.
  • schema is a standard JSON Schema (draft 2020-12). One entry per node type that has a config; START has none.

Request

query NodeSchemas {
nodeConfigSchemas {
type
schema
}
}
variables
{}

Allowed values

  • NodeType: ACTION, APPROVAL, AWAIT_REPLY, CONDITION, END_SESSION, GENERATE_ARTIFACT, KNOWLEDGE_SEARCH, LLM, PDF_EXPORT, RESPONSE, ROUTER, SET_VARIABLE, START, SUB_AGENT, SWITCH, TOOL, WEBHOOK

Code samples

nodeConfigSchemas.sh
#!/usr/bin/env bash
# nodeConfigSchemas: The JSON Schema of every node type's config, for building or validating graphs in your own tools.
# Credential: API key (WETEL_API_KEY).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "X-Api-Key: $WETEL_API_KEY" \
--data-binary @- <<'JSON'
{
"query": "query NodeSchemas{nodeConfigSchemas{type schema}}",
"variables": {}
}
JSON

Responses

Success (one entry of the list shown) (HTTP 200)

{
"data": {
"nodeConfigSchemas": [
{
"type": "RESPONSE",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"maxVisits": {
"type": "number"
},
"messageTemplate": {
"type": "string"
},
"mood": {
"type": "string"
},
"structuredContentTemplate": {
"type": "string"
}
},
"additionalProperties": {}
}
}
]
}
}
UNAUTHENTICATED: No credential (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"nodeConfigSchemas"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}

List the runs of one session, newest first, to debug what a workflow did.

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • A run is one execution of the graph for one turn. Pass a run’s id to workflowRun(id) for the full node trace.

Request

query Runs($sessionId: Int!) {
workflowRuns(sessionId: $sessionId) {
id
status
workflowId
errorMessage
totalTokensUsed
createdAt
completedAt
}
}
variables
{
"sessionId": 4211
}

Allowed values

  • WorkflowRunStatus: AWAITING_INPUT, COMPLETED, FAILED, INTERRUPTED, PENDING, RUNNING

Code samples

workflowRuns.sh
#!/usr/bin/env bash
# workflowRuns: List the runs of one session, newest first, to debug what a workflow did.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query Runs($sessionId:Int!){workflowRuns(sessionId:$sessionId){id status workflowId errorMessage totalTokensUsed createdAt completedAt}}",
"variables": {
"sessionId": 4211
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"workflowRuns": [
{
"id": 5531,
"status": "COMPLETED",
"workflowId": 12,
"errorMessage": null,
"totalTokensUsed": 412,
"createdAt": "2026-10-12T03:15:02.300Z",
"completedAt": "2026-10-12T03:15:05.902Z"
}
]
}
}
UNAUTHENTICATED: Called with an API key instead of a JWT (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowRuns"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}

Read one run’s node trace and context, to see exactly which nodes ran, in order, and why.

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • nodeTrace is a JSON array with one entry per finished node (nodeId, nodeType, startedAt, finishedAt and, depending on the node, outputVar, outputValue, tokensUsed, a status and error details). context is the run’s variables. Secrets and signed URLs are masked on read.
  • A run that stopped at an await_reply or approval node is AWAITING_INPUT; the run that continued it carries its id in resumedFromRunId, and the paused run points forward with continuedByRunId.
  • Returns null (not an error) for an id that does not exist in your workspace.

Request

query Run($id: Int!) {
workflowRun(id: $id) {
id
status
sessionId
workflowId
errorMessage
nodeTrace
context
resumedFromRunId
continuedByRunId
totalTokensUsed
createdAt
completedAt
}
}
variables
{
"id": 5531
}

Allowed values

  • WorkflowRunStatus: AWAITING_INPUT, COMPLETED, FAILED, INTERRUPTED, PENDING, RUNNING

Code samples

workflowRun.sh
#!/usr/bin/env bash
# workflowRun: Read one run's node trace and context, to see exactly which nodes ran, in order, and why.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query Run($id:Int!){workflowRun(id:$id){id status sessionId workflowId errorMessage nodeTrace context resumedFromRunId continuedByRunId totalTokensUsed createdAt completedAt}}",
"variables": {
"id": 5531
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"workflowRun": {
"id": 5531,
"status": "COMPLETED",
"sessionId": 4211,
"workflowId": 12,
"errorMessage": null,
"nodeTrace": [
{
"nodeId": "start",
"nodeType": "start",
"startedAt": "2026-10-12T03:15:02.310Z",
"finishedAt": "2026-10-12T03:15:02.311Z"
},
{
"nodeId": "classify",
"nodeType": "llm",
"startedAt": "2026-10-12T03:15:02.312Z",
"finishedAt": "2026-10-12T03:15:04.870Z",
"outputVar": "category",
"outputValue": "network",
"tokensUsed": 412
},
{
"nodeId": "reply",
"nodeType": "response",
"startedAt": "2026-10-12T03:15:04.871Z",
"finishedAt": "2026-10-12T03:15:05.902Z"
}
],
"context": {
"userMessage": "My laptop cannot connect to the office VPN since this morning.",
"category": "network"
},
"resumedFromRunId": null,
"continuedByRunId": null,
"totalTokensUsed": 412,
"createdAt": "2026-10-12T03:15:02.300Z",
"completedAt": "2026-10-12T03:15:05.902Z"
}
}
}

Not in your workspace (HTTP 200)

{
"data": {
"workflowRun": null
}
}
UNAUTHENTICATED: Called with an API key instead of a JWT (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowRun"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": {
"workflowRun": null
}
}

List the conversations waiting at an await_reply node for the user’s answer, newest first.

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Pass sessionId to check one conversation (is it stuck, or waiting for the user?); omit it for every open wait in the workspace. Only WAITING rows are listed.
  • [object Object]
  • options is the numbered list the user was shown (empty in FREE_TEXT mode). The stored run context is never returned here.

Request

query Waiting($sessionId: Int) {
pendingAwaitReplies(sessionId: $sessionId) {
id
sessionId
workflowRunId
nodeId
status
matchMode
saveAs
options {
slot
id
label
}
expiresAt
}
}
variables
{
"sessionId": 4211
}

Allowed values

  • WorkflowRunWaitStatus: CANCELLED, EXPIRED, RESUMED, WAITING
  • AwaitReplyMatchMode: FREE_TEXT, OPTIONS

Code samples

pendingAwaitReplies.sh
#!/usr/bin/env bash
# pendingAwaitReplies: List the conversations waiting at an await_reply node for the user's answer, newest first.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query Waiting($sessionId:Int){pendingAwaitReplies(sessionId:$sessionId){id sessionId workflowRunId nodeId status matchMode saveAs options{slot id label}expiresAt}}",
"variables": {
"sessionId": 4211
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"pendingAwaitReplies": [
{
"id": 207,
"sessionId": 4211,
"workflowRunId": 5534,
"nodeId": "ask_device",
"status": "WAITING",
"matchMode": "OPTIONS",
"saveAs": "device",
"options": [
{
"slot": 1,
"id": "laptop",
"label": "Laptop"
},
{
"slot": 2,
"id": "desktop",
"label": "Desktop"
}
],
"expiresAt": "2026-10-13T03:16:10.000Z"
}
]
}
}
BAD_USER_INPUT: Session id sent as a string (HTTP 400)

Sent with variables:

{
"sessionId": "four-two-one-one"
}
{
"errors": [
{
"message": "Variable \"$sessionId\" got invalid value \"four-two-one-one\"; Int cannot represent non-integer value: \"four-two-one-one\"",
"locations": [
{
"line": 1,
"column": 15
}
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
]
}

Run an agent’s published workflow headlessly (no chat message). Asynchronous; returns once accepted.

  • Kind: mutation
  • Credential: API key: X-Api-Key: sk_..., kept on your server only
  • Available in: sandbox and production
  • A successful answer means the task was accepted and queued, never that it succeeded. Read the result with workflowTaskOutcome and the returned sessionId.
  • variables is a flat JSON object merged into the run’s context; externalConversationId groups repeat calls for one conversation into one session.
  • The agent must be ACTIVE and have a workflow attached. Limited to 60 calls per minute per client IP by default.

Request

mutation RunTask($input: RunWorkflowTaskInput!) {
runWorkflowTask(input: $input) {
accepted
sessionId
workflowId
reusedSession
turnCount
}
}
variables
{
"input": {
"agentId": 7,
"externalConversationId": "ticket-1002",
"variables": {
"ticketId": "T-1002",
"subject": "Cannot print to the 3rd floor printer",
"reporterRole": "staff"
}
}
}

Input fields

RunWorkflowTaskInput

FieldTypeRequiredDescription
agentIdInt!yesId of an ACTIVE agent of the API key workspace with a workflow attached; NotFound for an unknown agent, BadRequest when it is inactive or has no workflow.
attachmentFilenameStringnoFilename for attachmentUrl (at most 255 characters).
attachmentMimeStringnoMIME type for attachmentUrl (at most 255 characters); when omitted it is taken from the filename extension, then from the URL path.
attachmentUrlStringnoSingle-file shortcut: absolute http(s) URL of one file. Equivalent to attachments: [{url, mime, filename}] with the attachmentMime and attachmentFilename fields below. Send EITHER this OR attachments, never both (both is refused with a 400).
attachments[MessageAttachmentInput!]noOptional files for this task (at most 10), the same structured shape and validation as attachments on sdkSendMessage: absolute http(s) URL, no embedded credentials, sanitised filename, MIME and kind inferred when omitted. Not fetched at accept time; a workflow node downloads them when it needs them. In workflow context they appear as ctx.attachments (list of {url, mime, filename, kind}), ctx.attachmentImageUrls (the IMAGE urls, usable as imageUrlsVar), and the first entry fills ctx.attachment, ctx.attachmentUrl, ctx.attachmentMime and ctx.attachmentFilename. Do not combine with the single attachmentUrl field (400).
clientTurnIdStringnoOptional opaque correlation token of your own (at most 255 characters), echoed on every AiResponseEvent this run publishes; not stored.
externalConversationIdString!yesYour own id for the conversation or job (1 to 255 characters). It becomes the session clientExternalId: calls with the same id reuse the open session for that agent, so they share history.
variablesJSONnoFacts for the run as a flat JSON object (send it through variables): at most 32 keys matching [A-Za-z][A-Za-z0-9_]* (64 characters at most), values string (at most 4096 characters), number, boolean or null. Keys starting with ”_” and runner names such as sessionId, userMessage or channel are refused. Merged into the run variables unchanged, never coerced.

MessageAttachmentInput

FieldTypeRequiredDescription
filenameStringnoOriginal filename, at most 255 characters. Quotes, square brackets and control characters are removed.
kindAttachmentKindnoKind of file. When omitted it is inferred from the MIME type (image/*, audio/*, video/*, PDF and Office or text types are DOCUMENT, anything else OTHER).
mimeStringnoMIME type, at most 100 characters (for example image/png). When omitted it is inferred from the filename, then from the URL path extension.
urlString!yesAbsolute http(s) URL of the file, at most 2048 characters, without user:password credentials. Treat a signed URL as a credential: it is redacted wherever messages are read back.

Allowed values

  • AttachmentKind: AUDIO, DOCUMENT, IMAGE, OTHER, VIDEO

Code samples

runWorkflowTask.sh
#!/usr/bin/env bash
# runWorkflowTask: Run an agent's published workflow headlessly (no chat message). Asynchronous; returns once accepted.
# Credential: API key (WETEL_API_KEY).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "X-Api-Key: $WETEL_API_KEY" \
--data-binary @- <<'JSON'
{
"query": "mutation RunTask($input:RunWorkflowTaskInput!){runWorkflowTask(input:$input){accepted sessionId workflowId reusedSession turnCount}}",
"variables": {
"input": {
"agentId": 7,
"externalConversationId": "ticket-1002",
"variables": {
"ticketId": "T-1002",
"subject": "Cannot print to the 3rd floor printer",
"reporterRole": "staff"
}
}
}
}
JSON

Responses

Accepted (HTTP 200)

{
"data": {
"runWorkflowTask": {
"accepted": true,
"sessionId": 4290,
"workflowId": 12,
"reusedSession": false,
"turnCount": 1
}
}
}
BAD_REQUEST: Agent has no workflow (HTTP 200)
{
"errors": [
{
"message": "Agent 7 has no workflow attached — runWorkflowTask requires one",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"runWorkflowTask"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "Agent 7 has no workflow attached — runWorkflowTask requires one",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}
NOT_FOUND: Agent not in the key’s workspace (HTTP 200)
{
"errors": [
{
"message": "Agent 7 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"runWorkflowTask"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Agent 7 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}

Read the outcome of a task started with runWorkflowTask, with the same API key.

  • Kind: query
  • Credential: API key: X-Api-Key: sk_..., kept on your server only
  • Available in: sandbox and production
  • status is the newest run’s status: PENDING and RUNNING mean poll again (with a back-off); COMPLETED, FAILED, INTERRUPTED and AWAITING_INPUT are final for now. null means the task is still queued.
  • replies are what the agent said, oldest first. The run context and node trace are never returned here.

Request

query TaskOutcome($sessionId: Int!) {
workflowTaskOutcome(sessionId: $sessionId) {
sessionId
status
runs {
id
status
errorMessage
}
replies {
text
createdAt
}
}
}
variables
{
"sessionId": 4290
}

Allowed values

  • WorkflowRunStatus: AWAITING_INPUT, COMPLETED, FAILED, INTERRUPTED, PENDING, RUNNING

Code samples

workflowTaskOutcome.sh
#!/usr/bin/env bash
# workflowTaskOutcome: Read the outcome of a task started with runWorkflowTask, with the same API key.
# Credential: API key (WETEL_API_KEY).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "X-Api-Key: $WETEL_API_KEY" \
--data-binary @- <<'JSON'
{
"query": "query TaskOutcome($sessionId:Int!){workflowTaskOutcome(sessionId:$sessionId){sessionId status runs{id status errorMessage}replies{text createdAt}}}",
"variables": {
"sessionId": 4290
}
}
JSON

Responses

Completed (HTTP 200)

{
"data": {
"workflowTaskOutcome": {
"sessionId": 4290,
"status": "COMPLETED",
"runs": [
{
"id": 5602,
"status": "COMPLETED",
"errorMessage": null
}
],
"replies": [
{
"text": "Thanks. Your ticket goes to the L2 queue; we will reply here.",
"createdAt": "2026-10-12T04:10:13.540Z"
}
]
}
}
}
NOT_FOUND: Not a task session of this key’s workspace (HTTP 200)
{
"errors": [
{
"message": "No workflow task found for session 4290",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workflowTaskOutcome"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "No workflow task found for session 4290",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}