Skip to main content
The function node runs one of ModuleX’s built-in registry functions as a step in your workflow. It is the node you reach for when you need to call an external HTTP API, deliver a signed webhook, or validate data against a JSON Schema — without writing a custom integration or an LLM prompt. A function node is configured with a FunctionNodeConfig and dispatched to exactly one registry function by name. There are exactly four built-in functions: The function registry is fixed: these four are the only callable functions. To call a third-party service that has a dedicated integration, use the tool node instead; to run a custom external service, connect a custom MCP server. For where the function node sits among the nine node kinds, see the node types overview.
Use the function node’s http_request for ad-hoc calls to APIs that ModuleX does not have a first-class integration for. When an integration already exists for the service, prefer the tool node — it handles authentication, credentials, and typed parameters for you.

How the function node works

The function node separates configuration into two channels, both verified against the engine dispatch (modulex:py/app/services/workflow_engine.py:2145):
  • input_mapping — runtime data resolved from earlier nodes via references. Each {{node_id.path}} reference is resolved against run state and passed to the function as its inputs argument. This is the data the function operates on (the body to send, the data to validate).
  • parameters — static configuration for the function, passed as its params argument (the URL, the HTTP method, the JSON Schema). These are set once when you build the node and are not resolved from run state.
At runtime the node:
  1. Resolves input_mapping against state into an inputs dictionary. References that resolve to None or "" are skipped. If input_mapping is empty, the deprecated input_keys list is used as a fallback (workflow_engine.py:2166).
  2. Reads parameters (the static config) into params.
  3. Looks up the function by function_name in the registry. An unknown name raises ValueError at graph-build time, listing the available functions (workflow_engine.py:2152).
  4. Calls func.execute(inputs, params) and writes the result into run state under the node’s own id.
The two {{ }} styles are not the same. References between nodes use the workflow engine’s {{node_id.path}} syntax and are resolved in input_mapping. Inside http_request and send_webhook, the literal {key} placeholder (single braces) is a separate, function-local templating step that substitutes values from the resolved inputs dictionary into URL strings and body/payload fields. See variables & references for the engine reference model.

Output convention

Like every node, the function node writes its result to run state under its node id (workflow_engine.py:2163). Downstream nodes read it with a reference such as {{node_http_1.body}} or {{node_validate_1.valid}}. The shape stored depends on the outcome: So when a function fails, {{node_id.success}} is false and {{node_id.error}} holds the message. You can branch on this with a conditional node — for example, route on {{node_http_1.success}} equals false.

Retries and error handling

Function nodes are retry-wrapped by the engine (workflow_engine.py:1245), but with an important caveat: the node-level retry only re-runs the step when the underlying function raises an exception. The four built-ins catch their own errors and return a FunctionResult with success: false instead of raising. That result is stored as the node output — it does not trigger a node-level retry. What this means in practice:
  • A failed http_request (timeout, connection error, non-2xx) returns success: false and is not retried by the node’s retry_config. Handle it downstream with a conditional, or use send_webhook (which has its own internal retry loop) when you need delivery retries.
  • send_webhook retries 5xx responses and timeouts internally up to its retry_count (capped at 5) with exponential backoff — independent of the node’s retry_config.
  • Node-level retry_config (max_attempts 1-10, initial_interval, backoff_factor, retry_on_error_types) still applies to truly unexpected exceptions. See error handling & retries.
The function node does not call a language model and does not retrieve managed knowledge, so it incurs no ModuleX credits on its own. Costs come only from what you call: http_request and send_webhook hit external endpoints (billed by that service, not ModuleX), and the validation functions run locally. Credits are metered for LLM, agent, and managed-knowledge work — see credits & metering.

Configuration: FunctionNodeConfig

Source: modulex:py/app/models/workflow_schemas.py:179.
string
required
The name of the built-in function to run. Must be one of http_request, send_webhook, validate_schema, or validate_workflow_schema. An unknown name fails graph compilation with a ValueError listing the available functions.
object
default:"{}"
Maps function input keys to values, resolved from run state. Use {{node_id.path}} references to pull values from earlier nodes. A pure {{ref}} value keeps its native type; mixed strings are templated. References resolving to None or "" are skipped. The resolved dictionary becomes the function’s inputs.
object
Static configuration passed to the function as its params (the URL, method, schema, and so on — see each function below). Not resolved from run state.
string[]
default:"[]"
deprecated
Legacy. A list of state keys to collect into inputs when input_mapping is empty. Use input_mapping with references instead.
string
deprecated
Legacy and ignored. Output is always stored under the node id.
A minimal config selecting a function and wiring one input looks like this:
Function node config
The function builder UI accepts the wrapped { "config": { … } } form and normalizes it to function_config; either shape is valid on the wire (workflow_schemas.py:686).

Built-in functions

Each function defines its parameters as a list of typed ParameterDefinitions (modulex:py/app/services/functions/base.py:23). Parameter types are string, number, boolean, object, array, or select (a dropdown with fixed options). A required parameter with no default that is missing fails the function with Missing required parameter: <name>.

http_request

Make an HTTP request to an external API. Source: modulex:py/app/services/functions/http_request.py:24. Category: integration.

Parameters (static parameters)

string
required
The URL to send the request to. Supports {key} placeholders substituted from the resolved inputs (for example https://api.example.com/users/{user_id}).
select
default:"GET"
HTTP method. One of GET, POST, PUT, PATCH, DELETE. Lower-cased input is upper-cased automatically.
object
default:"{}"
Custom request headers, for example {"Authorization": "Bearer token"}. A string value is parsed as JSON, then as a Python literal, falling back to an empty object if neither parses.
object
Request body for POST, PUT, and PATCH. A value of exactly {key} is replaced by that input’s native value (preserving objects and arrays); other strings are templated. Ignored for GET/DELETE. If body is null and use_inputs_as_body is unset, the resolved inputs are used as the body.
boolean
default:"false"
If true, the entire resolved inputs dictionary is used as the request body, ignoring body.
select
default:"json"
Content encoding for the body. One of json, form, or text.
number
default:"30"
Request timeout in seconds.
select
default:"none"
Authentication scheme. One of none, bearer, basic, or api_key.
string
The token, password, or API key value. For bearer it becomes Authorization: Bearer <auth_value>; for api_key it is sent under api_key_header; for basic it is the password.
string
Username for basic auth (paired with auth_value as the password).
string
default:"X-API-Key"
Header name used to send the API key when auth_type is api_key.
boolean
default:"true"
Whether to follow HTTP redirects.
boolean
default:"true"
Whether to verify the server’s TLS certificate.
http_request is a general outbound HTTP client. Treat auth_value, tokens, and any secret headers as sensitive: set them in parameters, and avoid logging response bodies that may echo them back. Do not point url at internal-only addresses you do not control.

Output

On a 2xx response, success is true and the node stores data:
number
The HTTP status code.
object
The response headers as a flat object.
object | string
The parsed JSON response body, or the raw text if the response is not JSON.
string
The final request URL (after any redirects).
The function also returns metadata ({method, elapsed_ms}); metadata is not stored in run state. A non-2xx status sets success to false and error to HTTP <status_code>, while still returning the same data body — so you can branch on the status. Transport failures return success: false with error of Request timeout after <n> seconds (timeout) or Request failed: <detail> (connection/request error). An unexpected error returns Unexpected error: <detail>.

send_webhook

Deliver a webhook with standard headers, optional HMAC signing, and internal retries. Source: modulex:py/app/services/functions/send_webhook.py:27. Category: integration. Always sent as POST with Content-Type: application/json.

Parameters (static parameters)

string
required
The webhook endpoint URL.
string
required
An event-type identifier, for example order.created or workflow.completed. Sent in the X-Webhook-Event header and, when metadata is included, in the payload.
object
The webhook payload. If omitted (null), the resolved inputs are used as the payload. String values containing {key} placeholders are templated from inputs.
string
A secret key. When set, the payload is signed with HMAC-SHA256 and the signature is sent as X-Webhook-Signature: sha256=<hex> (see signing below).
object
default:"{}"
Additional custom headers, merged on top of the standard webhook headers.
number
default:"30"
Per-attempt request timeout in seconds.
number
default:"3"
Number of retry attempts on retriable failure. Capped at 5; values above 5 are clamped.
number
default:"1"
Base delay in seconds between retries. Each retry waits retry_delay × 2^attempt, capped at 30 seconds.
boolean
default:"true"
When true, the delivered body is wrapped as {webhook_id, event_type, timestamp, data} with your payload under data. When false, the payload is sent as-is.

Standard headers and signing

Every delivery includes these headers (your custom headers are merged on top):
Standard webhook headers
The signature is HMAC-SHA256(key = signing_secret, message = "<timestamp>.<payload_json>"), hex-encoded and prefixed with sha256=. Receivers should recompute the HMAC over the same timestamp.payload string to verify authenticity.
A 5xx response or a timeout is retried up to retry_count times with exponential backoff. A 4xx response is treated as non-retriable and fails immediately. These are the function’s own retries and are independent of the node’s retry_config.

Output

On a 2xx delivery, success is true and the node stores data:
string
The generated webhook id (a UUID4), matching the X-Webhook-ID header.
number
The HTTP status returned by the endpoint.
object | string
The endpoint’s response body, parsed as JSON when possible, otherwise raw text.
array
Per-attempt records, each {attempt, status_code | error, elapsed_ms}.
number
The unix timestamp used for the delivery and signature.
A 4xx failure returns success: false, error of Webhook delivery failed: HTTP <code>, and data with {webhook_id, status_code, response (truncated to 500 chars), attempts}. When all retries are exhausted, error is Webhook delivery failed after <n> attempts: <last_error> with data of {webhook_id, attempts}.

validate_schema

Validate data against a JSON Schema (Draft-7). Source: modulex:py/app/services/functions/validate_schema.py:28. Category: validation.

Parameters (static parameters)

object
required
The JSON Schema definition to validate against, for example {"type": "object", "properties": {…}}.
string
The key in the resolved inputs to validate. If unset, the entire inputs dictionary is validated. A data_key that is not present in inputs fails with Input key '<key>' not found in inputs.
boolean
default:"false"
When true, validation stops at the first error. When false, all errors are collected and returned together.
boolean
default:"false"
When true, attempts simple type coercion before validating (string→number/integer, string→boolean for true/1/yes and false/0/no, value→string), recursing into array items and object properties named in the schema.

Output

On valid data, success is true and the node stores data:
boolean
true when the data conforms to the schema.
array
An empty array on success.
object
The validated data (after any coercion).
On invalid data (non-strict mode), success is false, error is Validation failed with <n> error(s), and data carries the detail:
boolean
false.
number
The number of validation errors.
array
null
null when validation failed.
An invalid schema returns error of Invalid schema: <detail>. If the jsonschema library is unavailable, the function returns an error rather than validating.

validate_workflow_schema

Validate a generated workflow package against ModuleX’s Pydantic WorkflowDefinition model. This is primarily used by the AI Composer when it generates a workflow, to catch structural errors before the workflow is saved or run. Source: modulex:py/app/services/functions/validate_workflow_schema.py:23. Category: validation.

Parameters (static parameters)

string
default:"generated_workflow"
The key in the resolved inputs that holds the workflow package. The package’s .workflow field (or the value itself, if it is not a package wrapper) is validated.

Behavior

Before Pydantic validation, the function rejects the virtual nodes __start__ and __end__ if they appear in the workflow’s nodes array — these are virtual and belong only in edges. This pre-check produces a virtual_node_error. The package is then validated against WorkflowDefinition (workflow_schemas.py:929).

Output

On a valid workflow, success is true and the node stores data with {valid: true, errors: [], validated_data: <full package>, message: "Workflow schema validation passed"}. On an invalid workflow, success is false, error is Workflow validation failed with <n> error(s), and data carries:
boolean
false.
number
The number of validation errors.
string[]
Formatted error strings, each <path>: <message> (got: <value>).
array
null
null when validation failed.
A missing data_key, a missing workflow key in the package, or an unexpected error each return success: false with a descriptive error.

Worked example: fetch, validate, deliver

This three-node fragment fetches an order from an external API, validates the response body against a JSON Schema, then delivers a signed webhook with the validated data. The first node uses static parameters for the URL and method; the second and third pull data from upstream nodes through input_mapping.
What happens at run time:
  1. node_fetch_order resolves {{node_input.order_id}} into inputs, substitutes it into the URL via the {order_id} placeholder, sends an authenticated GET, and stores {status_code, headers, body, url} under node_fetch_order.
  2. node_validate_order reads {{node_fetch_order.body}} as order, validates it against the schema, and stores {valid, errors, validated_data} under node_validate_order. If invalid, success is false and validated_data is null.
  3. node_notify reads {{node_validate_order.validated_data}} as the payload, wraps it with metadata, signs it, and POSTs it — retrying 5xx/timeouts internally up to three times.
To stop the webhook firing when validation fails, insert a conditional node between nodes 2 and 3 that branches on {{node_validate_order.valid}}. To pause for a human before a risky delivery, use an interrupt node.

Running the workflow

You build and run function-node workflows the same way as any other workflow — from the builder, or programmatically. The example below runs a saved workflow over the API and SDKs. Authentication uses Authorization: Bearer mx_live_… plus the X-Organization-ID header on every request; see authentication.
The run endpoint sits behind ModuleX’s usage gate: when an org is out of credits or over a limit, the run surface returns a flat DenialEnvelope ({code, layer, key, current, limit, reason}) as 402, 403, or 429, rather than a normal error. See usage gating & limits and errors & status codes. To stream the function node’s results live as the run executes, see SSE run streaming.

Node types overview

How the function node fits among the nine node kinds and how each writes to run state.

Tool node

Call a first-class integration tool instead of a raw HTTP request.

Variables & references

The {{node_id.path}} reference model used in input_mapping.

Conditional node

Branch on a function node’s success or valid output.