Skip to content

Action Node

The action node invokes a registered Custom Action — a single, direct HTTP endpoint registered ahead of time, with no MCP protocol involved. It’s the right fit when you have an existing REST API and don’t want to stand up a full MCP server just to make one or two endpoints callable from a workflow. If you already have (or plan to build) a proper MCP server exposing multiple tools with schemas, use a tool node instead.

See Workflows Overview for how this node type fits into the broader node/edge model.

ActionNodeConfig:

FieldTypeRequiredDescription
customActionIdnumberYes, unless fetchAsBase64Var is setThe ID of the registered Custom Action to call, scoped to your tenant. Not read at all in fetchAsBase64Var mode — see Fetching a file as base64 below.
certifiedOperationIdnumberNoA Wetel-certified operation to call instead of hand-authoring the full request body — see Certified operations below. Additive; leave unset to keep authoring argsTemplate as a full request body yourself.
argsTemplatestringNoA Handlebars-templated JSON string providing the action’s call arguments. Same convention as a tool node’s argsTemplate — values can reference context variables from earlier nodes, and the template is rendered with JSON-safe escaping (quotes, backslashes, control characters all handled for you). When certifiedOperationId is set to a GraphQL (query/mutation) operation, this is that operation’s GraphQL variables, not the full request body — see below. For a REST certified operation (rest_get/rest_post), or no certified operation at all, this is the full request body/params, unchanged. Ignored entirely in fetchAsBase64Var mode.
outputVarstringYesThe context variable the action’s response (or, in fetchAsBase64Var mode, the base64-encoded file) is stored in.
timeoutMsnumberNoRequest timeout in milliseconds. Defaults to 10000 (10 seconds). Clamped to a hard ceiling of 30000 (30 seconds) regardless of what you configure.
fetchAsBase64VarstringNoAn alternative to the normal customActionId-driven call — see Fetching a file as base64 below.
extractTextVarstringNoAdded 2026-09-17. Redirects fetchAsBase64Var’s fetch to write extracted plain text instead of base64-encoded bytes — see Extracting text instead of raw bytes below. Requires fetchAsBase64Var to also be set.
extractTextMaxCharsnumberNoAdded 2026-09-17. Overrides the default 50,000-character cap on extractTextVar’s output. Clamped to a hard ceiling of 200,000. Ignored when extractTextVar is unset.

For OmniChat, Nortia, and aisCRM actions specifically, you don’t have to hand-write the request yourself. Set certifiedOperationId to a hand-verified operation from Wetel’s own catalog. For a GraphQL operation, argsTemplate becomes that operation’s GraphQL variables instead of the full request body — the node builds { "query": "<the certified operation's text>", "variables": <your argsTemplate> } for you. For a REST operation, argsTemplate is unchanged — still the full request body/params — since there’s no query text to wrap; see the REST callout below.

{
"id": "list_omnichat_contacts",
"type": "action",
"label": "List OmniChat Contacts",
"config": {
"customActionId": 9,
"certifiedOperationId": 12,
"argsTemplate": "{}",
"outputVar": "contacts_result"
},
"position": { "x": 250, "y": 220 }
}

The certified operation’s compatible credential sources must include the action’s own credentialSource, checked on every run — pairing a Nortia-only certified operation with an OmniChat-credentialed action fails loudly at execution time, naming both values, rather than silently misbehaving.

In the Workflow Editor UI, this shows up as a second “Certified operation” dropdown on the action node’s config panel, which only appears once the chosen Custom Action’s credential source has a certified catalog — plus a read-only preview of the real query text once you pick one. See Certified Operations for the full GraphQL and REST reference, including which credential sources are realistically self-serve today.

Not every certified operation is GraphQL. As of 2026-09-04 the catalog also has rest_get/rest_post entries (some Nortia operations run against a plain REST API alongside Nortia’s GraphQL admin API). For those, argsTemplate is the full request body/params directly — there’s no { query, variables } wrapper, exactly as if you’d hand-authored the request yourself. Check an operation’s operationType (via certifiedOperation(id)) before assuming the GraphQL-variables framing above applies.

An alternative mode to the normal customActionId-driven call: set fetchAsBase64Var to the name of a context variable that holds a URL (for example, an attachment URL from an earlier node), and the node fetches that URL’s bytes server-side, base64-encodes them, and writes the result into ctx[outputVar]. customActionId, argsTemplate, certifiedOperationId, and every other field on this node are ignored in this mode — it’s a plain, unauthenticated GET against a URL your workflow already trusts, through the same SSRF-guarded outbound path every other action node call uses.

This exists specifically to feed a certified operation whose request body expects a base64-encoded file field (for example, a file_base64 parameter on a REST rest_post operation) — chain a fetchAsBase64Var node immediately before the node that actually sends the file:

{
"id": "fetch_resume_bytes",
"type": "action",
"label": "Fetch Résumé as Base64",
"config": {
"fetchAsBase64Var": "resumeFileUrl",
"outputVar": "resumeBase64"
},
"position": { "x": 250, "y": 180 }
}

The response is capped at 10 MB (matching the largest file-upload certified operation in the catalog today) — a larger file fails the node rather than truncating silently. Note base64 inflates the stored context value by roughly a third versus the raw file size.

In the Workflow Editor UI, this is a single text field at the top of the action node’s config panel — filling it in hides the custom-action picker, certified-operation picker, and arguments fields below, since none of them are read in this mode.

fetchAsBase64Var also accepts an s3://bucket/key URI, alongside an ordinary HTTP(S) URL (added 2026-09-22, and shared with the webhook node’s equivalent field). This is fetched with the platform’s own AWS credentials, not anything tenant-specific — the platform’s own configured bucket is always denied, and any other bucket must be explicitly granted to your tenant first (set operationally today, not via a GraphQL mutation — ask if you need one granted). A presigned S3/CloudFront HTTPS URL needs none of this; it’s just an ordinary HTTPS fetch.

Added 2026-09-17. extractTextVar is a second output mode for fetchAsBase64Var’s fetch — instead of base64-encoding the fetched bytes into ctx[outputVar], it extracts plain text from them and writes that into ctx[extractTextVar] instead. fetchAsBase64Var still names which context variable holds the URL to fetch; extractTextVar only redirects what happens to the fetched bytes and where the result lands. When extractTextVar is set, ctx[outputVar] is not populated at all — ctx[extractTextVar] is the only output.

This is for mid-conversation document extraction (e.g. a partner API hands back a PDF/spreadsheet URL and you want an llm node to read its contents), as distinct from Knowledge Base (RAG) ingestion, which is for documents you want embedded and retrievable across many future turns/sessions.

{
"id": "fetch_and_extract_report",
"type": "action",
"label": "Fetch and Extract Report Text",
"config": {
"fetchAsBase64Var": "reportFileUrl",
"extractTextVar": "reportText",
"extractTextMaxChars": 20000,
"outputVar": "unused_when_extractTextVar_is_set"
},
"position": { "x": 250, "y": 200 }
}

Behavior:

  • The fetched bytes are routed by the response’s declared Content-Type header through the same extractor registry the Knowledge Base file-upload pipeline uses — so this supports the identical five formats: PDF, DOCX, Markdown, CSV, and XLSX. An unrecognized or unsupported Content-Type fails the node with a clear error naming the supported mime types, rather than silently returning empty/garbage text.
  • Extracted text is capped at extractTextMaxChars (default 50,000 characters, hard ceiling 200,000 regardless of what you configure) — comfortably inside a single LLM prompt’s context budget without needing chunking.
  • ctx["${extractTextVar}Truncated"] — a boolean, always written (true or false, never omitted) — records whether truncation happened, so your graph can branch on a partial extraction (e.g. route to “this document is too long, please summarize the relevant section”) instead of silently answering from a cut-off document.
  • This is a separate, distinct cap from the general fetchAsBase64Var download itself, which is still capped at 10 MB regardless of whether extractTextVar is set — extraction happens as a post-processing step on the already-downloaded bytes, it doesn’t change what’s fetched.
  • Never round-trips through base64 — there’s no reason to pay that string-inflation cost for a text-only downstream use.

An order-status assistant that, once it has extracted an order ID, calls a registered Custom Action wrapping a plain REST lookup endpoint:

{
"id": "lookup_order_action",
"type": "action",
"label": "Look Up Order via REST",
"config": {
"customActionId": 7,
"argsTemplate": "{\"orderId\": \"{{orderId}}\"}",
"outputVar": "order_lookup_result",
"timeoutMs": 8000
},
"position": { "x": 250, "y": 220 }
}

Followed by a response node that passes the result straight through:

{
"id": "respond_order_lookup",
"type": "response",
"label": "Respond with Order Details",
"config": {
"messageTemplate": "Order {{orderId}}: {{order_lookup_result}}",
"mood": "helpful"
},
"position": { "x": 350, "y": 220 }
}

Same opt-in two-port routing a tool node supports — draw one outgoing edge labeled success and one labeled failure (in the Workflow Editor UI, the card toolbar’s “Add success/failure branch” toggle) and the runner routes to failure on any thrown error (a real HTTP failure, a timeout, a malformed argsTemplate) instead of failing the whole run. The error message lands in a _lastNodeError context variable. A failure edge without a matching success edge fails validation at publish time. A node with neither label keeps today’s behavior — every outgoing edge fires unconditionally, and an unhandled failure fails the run.

This is separate from, and does not change, the existing session-identity outcome for SESSION_AISCRM/SESSION_OMNICHAT credential sources (NOT_SIGNED_IN / NOT_LINKED / NO_GRANT / NEEDS_ORG_CHOICE, exposed via ${outputVar}Status) — that’s a distinct “is the caller authorized” outcome, still read by a downstream router/condition node the same way it always has been. The success/failure edge only reflects whether the underlying network call itself succeeded.

  • timeoutMs is clamped to 30 seconds no matter what you set. If the underlying endpoint genuinely needs longer, an action node isn’t the right fit for that call.
  • argsTemplate has zero schema awareness, same as a tool node’s. Nothing validates it against the target endpoint’s expected request shape at save time or execution time — if the endpoint’s contract changes, you have to update argsTemplate yourself.
  • Custom Actions carry only a single optional bearer-token credential, plus an extraHeaders map for non-secret static headers — there’s no query-param credential option, and extraHeaders is explicitly NOT for secrets (see Custom Actions API: Extra static headers). It covers the “target API needs one more static header alongside the credential header” case (e.g. an x-organization-id identifier), not “target API needs a second secret.” The one credential slot isn’t locked to Authorization: Bearer, though — credentialSecretHeaderName sends the stored secret under any static header name instead (X-Api-Key, X-Client-Secret, etc.), with no Bearer prefix. If your endpoint needs more than one secret header, or a credential in a query parameter, you’ll need a small proxy of your own in front of it that accepts a bearer token and translates it. See MCP Connectors for a comparison of when to use an MCP connector vs. a Custom Action.
  • Never point argsTemplate at a bare "{{someVar}}" where someVar already holds a pre-serialized JSON string — this double-escapes the value and reliably fails JSON.parse(). Extract plain scalar values into separate variables and write the JSON object structure directly in argsTemplate.
  • For “match this human value to an internal id” lookups feeding into argsTemplate, use the findId template helper, not an LLM node — see Workflow Best Practices for why and how.
  • A failed action call surfaces a generic reply to the user, not the real error. The underlying failure (an HTTP error, a JSON parse failure) is available in the workflow run’s node trace and in a live NodeFailedEvent on the session subscription — check those when debugging, not the chat transcript.
  • To see exactly what argsTemplate actually rendered to, check renderedRequest on this node’s nodeTrace entry — captured on both success and failure. See Observability & Debugging Your Agent for the full nodeTrace shape.
  • fetchAsBase64Var is a plain, unauthenticated GET — same SSRF protection as every other action node target (a private/internal address is rejected), but it sends no Authorization header or credential of any kind. Only point it at a URL that’s meant to be publicly fetchable without auth (e.g. a signed/pre-authorized attachment URL from a partner’s API), not an endpoint that itself requires a bearer token.
  • fetchAsBase64Var performs no content-type or magic-byte check on what it fetches — it base64-encodes whatever bytes the URL returns, regardless of what the sender declared the file to be (see Channels: attachment markers for where a channel-supplied mime comes from). If your workflow cares that the fetched bytes are genuinely, say, a PDF, assert on the fetched content downstream rather than trusting the declared MIME type.
  • Redirects are not followed on action node calls (as of 2026-09-08). A customActionId-driven call rejects any 3xx response outright rather than following it — no action node target has a legitimate need to redirect, so this closes an SSRF gap rather than adding a real restriction. If your target legitimately redirects (e.g. an http://https:// upgrade, a trailing-slash normalization), point url on the underlying Custom Action directly at the final URL instead. A hostname that doesn’t resolve is also rejected, both when the Custom Action is saved and on every call.
  • On a Custom Action whose credentialSource is a Wetel-provisioned shared credential (OMNICHAT/NORTIA/AISCRM/LARK_TENANT), url/method can no longer be changed via updateCustomAction. Create a new Custom Action (and repoint the action node’s customActionId) instead of retargeting one carrying a shared Wetel credential. Custom actions using your own session identity (SESSION_*) or your own bearer token are unaffected.
  • extractTextVar requires fetchAsBase64Var to also be set — it’s an output-mode switch on that fetch, not a standalone mechanism. Setting it without fetchAsBase64Var fails validation when you save the workflow (updateWorkflow) and again at publishWorkflow.