Action Node
This content is not available in your language yet.
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.
Config fields
Section titled “Config fields”ActionNodeConfig:
| Field | Type | Required | Description |
|---|---|---|---|
customActionId | number | Yes, unless fetchAsBase64Var is set | The 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. |
certifiedOperationId | number | No | A 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. |
argsTemplate | string | No | A 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. |
outputVar | string | Yes | The context variable the action’s response (or, in fetchAsBase64Var mode, the base64-encoded file) is stored in. |
timeoutMs | number | No | Request timeout in milliseconds. Defaults to 10000 (10 seconds). Clamped to a hard ceiling of 30000 (30 seconds) regardless of what you configure. |
fetchAsBase64Var | string | No | An alternative to the normal customActionId-driven call — see Fetching a file as base64 below. |
extractTextVar | string | No | Added 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. |
extractTextMaxChars | number | No | Added 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. |
Certified operations
Section titled “Certified operations”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.
Fetching a file as base64
Section titled “Fetching a file as base64”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.
Extracting text instead of raw bytes
Section titled “Extracting text instead of raw bytes”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-Typeheader 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 unsupportedContent-Typefails 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 (trueorfalse, 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
fetchAsBase64Vardownload itself, which is still capped at 10 MB regardless of whetherextractTextVaris 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.
Worked example
Section titled “Worked example”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 }}Success/failure edges
Section titled “Success/failure edges”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.
Gotchas
Section titled “Gotchas”timeoutMsis 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.argsTemplatehas 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 updateargsTemplateyourself.- Custom Actions carry only a single optional bearer-token credential, plus an
extraHeadersmap for non-secret static headers — there’s no query-param credential option, andextraHeadersis 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. anx-organization-ididentifier), not “target API needs a second secret.” The one credential slot isn’t locked toAuthorization: Bearer, though —credentialSecretHeaderNamesends the stored secret under any static header name instead (X-Api-Key,X-Client-Secret, etc.), with noBearerprefix. 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
argsTemplateat a bare"{{someVar}}"wheresomeVaralready holds a pre-serialized JSON string — this double-escapes the value and reliably failsJSON.parse(). Extract plain scalar values into separate variables and write the JSON object structure directly inargsTemplate. - For “match this human value to an internal id” lookups feeding into
argsTemplate, use thefindIdtemplate 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
NodeFailedEventon the session subscription — check those when debugging, not the chat transcript. - To see exactly what
argsTemplateactually rendered to, checkrenderedRequeston this node’snodeTraceentry — captured on both success and failure. See Observability & Debugging Your Agent for the fullnodeTraceshape. fetchAsBase64Varis a plain, unauthenticated GET — same SSRF protection as every otheractionnode target (a private/internal address is rejected), but it sends noAuthorizationheader 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.fetchAsBase64Varperforms 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-suppliedmimecomes 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
actionnode calls (as of 2026-09-08). AcustomActionId-driven call rejects any 3xx response outright rather than following it — noactionnode 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. anhttp://→https://upgrade, a trailing-slash normalization), pointurlon 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
credentialSourceis a Wetel-provisioned shared credential (OMNICHAT/NORTIA/AISCRM/LARK_TENANT),url/methodcan no longer be changed viaupdateCustomAction. Create a new Custom Action (and repoint theactionnode’scustomActionId) instead of retargeting one carrying a shared Wetel credential. Custom actions using your own session identity (SESSION_*) or your own bearer token are unaffected. extractTextVarrequiresfetchAsBase64Varto also be set — it’s an output-mode switch on that fetch, not a standalone mechanism. Setting it withoutfetchAsBase64Varfails validation when you save the workflow (updateWorkflow) and again atpublishWorkflow.
Next steps
Section titled “Next steps”- Tool Node — the MCP-based alternative, for a full MCP server with multiple discoverable tools.
- Webhook Node — for a one-off HTTP call with no registration step at all.
- Workflows Overview — the full node/edge model.
- Workflow Best Practices — templating rules and the
findIdhelper. - LLM Node, Condition Node, Router Node, Response Node, Sub-Agent Node — other node types.
- Worked Examples & Demos — complete multi-node workflows.
- Workflows API Reference — the mutations for creating and publishing workflows.