Skip to content
Expedify
Logic & Data

Module · Logic

Custom Function

Lesson 5 of 11 · 7 min

Sooner or later a workflow needs something the palette does not have. Reformat a phone number into international form. Work out which of five price bands a deal falls into. Turn a supplier's oddly shaped JSON into the fields your CRM expects.

Custom Function is the escape hatch: a node that runs Python you write, in the middle of a workflow, with the data flowing through it.

Reach for it last, not first. Code is the part of a workflow that other people cannot read, the builder cannot validate, and nobody will want to change in a year. Check that Transform Data, a Condition or a Set Variable cannot do it before you write a function — most of the time one of them can.

The shape of the code

Define a function called process that takes inputs and context, and return whatever the next node should receive. What you return lands in {{alias.output}}.

the function in the example below
def process(inputs, context):
    raw = str(inputs.get("raw_phone") or "")
    digits = "".join(ch for ch in raw if ch.isdigit())
    if len(digits) == 10:
        digits = "91" + digits
    return {"e164": "+" + digits if digits else "", "digits": len(digits)}
Returns a dict, so downstream nodes can read {{customfunction_1.output.e164}}.

Returning a dictionary rather than a bare value is worth doing by habit. A dict gives every piece of what you computed its own name, so the next node reads output.e164 instead of an unlabelled output that nobody can interpret six months later.

Getting data in

The function does not automatically see the workflow. You declare what it needs in input_sources — a map of variable name to template — and read them from inputs.

The four fields that matter. `input_mapping` beside them is the legacy version of `input_sources`.

code

What it holds
Python code to execute

input_sources

What it holds
Structured input sources mapping - maps variable names to template expressions

timeout

What it holds
Execution timeout in seconds Defaults to 60.

capture_output

What it holds
Capture stdout/stderr output Defaults to true.

A few lines of Python in the middle of a workflow

Click the function node to see what it is given and how long it may take.

Scroll for all 3 steps →

Declaring inputs explicitly is more typing than a function that reaches for anything it likes, and it is the reason this node is debuggable. The panel states exactly what the function depends on, so a reference that breaks upstream is visible here rather than inside the code.

Watch out: Escape hatch: define process(inputs, context); read an upstream node via inputs.get("<alias>").

What you can import

The Requirements field does not install anything. It looks like a package list and it is a hint — it feeds the UI's picker and the AI code generator. Nothing is pip-installed when the workflow runs, so only libraries already present in the worker image can be imported. Anything else raises ImportError at run time, not when you save. Verified in the node source, 2026-08-01.

In practice this is less limiting than it sounds — the standard library is there, and the things a workflow usually wants (json, re, datetime, math) are all standard. But test an import before you build a workflow around it.

Time, and failure

timeout

Behaviour
Seconds before the function is killed. 60 by default. A function that calls an external service should set this deliberately rather than inherit it.

capture_output

Behaviour
On by default. Anything you print is captured and shows in the execution log — which is the main way to debug a function that returns the wrong thing.

When a function raises, the node fails and the run stops there. That is usually what you want. If the function is doing something optional — enriching a record that may not have the data — catch the exception inside and return a sentinel your next node can branch on, rather than letting one missing field kill the run.

When it is the right answer

The last row is the honest scope of this node.

Pull a value out of a nested response

Better tool
transform_data

Compare something and branch

Better tool
condition

Hold a value for later steps

Better tool
variable_set

Fetch something from an API

Better tool
api_request

Reshape, compute or validate in a way none of the above express

Better tool
custom_function

The good uses share a shape: a small, pure transformation with a name you could say out loud — normalise a phone number, score a lead, map their statuses onto ours. When a function starts fetching things and updating records, those parts belong in nodes around it, where the workflow can show what is happening.

Try it

  1. Add a Custom Function and give it one input in input_sources pointing at a field from your trigger.
  2. Return {"seen": inputs.get("your_field")} and run it, then read {{customfunction_1.output.seen}} in a node downstream.
  3. Add a print() and find it in the execution log.
  4. Now import requests at the top and run it again — if it raises ImportError, you have just proved the Requirements field to yourself.

Next: Human Approval — for the step that should not happen until a person says so.