Test
Use a Test node in your Flow to define one or more test cases directly on the Workspace canvas. Each case specifies an input message, an expected result, and the downstream node whose incoming message the expected result is asserted against.
A Test node acts as a stand-in for whatever normally starts the flow, such as an HTTP In or Inject node. Wire it into the flow at the point where that real entry point would sit. When a case runs, the node sends its message down that wire and the rest of the flow up to the designated Output node executes for real.
Case Fields
A single Test node can hold a list of cases. Every case is made up of five fields.
Label
Display name for the case. Shown in the Testing panel and in test results.
Message
The input msg object, entered as JSON. This is sent down the wire in place of a real trigger payload.
Expected
A JSON object asserted against whatever arrives at Output. The match is partial — see Match Semantics.
Output
The ID of the downstream node to watch. The test runner listens for a message arriving at this specific node.
Timeout
Milliseconds to wait for a message at Output before the case is marked as failed. Defaults to 5000.
How a Test Case Runs
Enable testing mode from the Testing tab in the Flow Editor sidebar.
Select the play button next to a test case to run it, or select the play button next to the name of the Test node to run all the test cases for that node. The Test node emits the case's Message from its single output port.
The message travels through the flow's normal wiring. Every node in between executes for real.
The runner watches the node named in Output. As soon as that node finishes processing the message,the results are compared against Expected.
If a match is found before Timeout elapses, the case passes and displays a green checkmark in the Testing panel. If the timeout elapses first, or if no matching message arrives, the case fails and displays a red X in the Testing panel with a diff comparison showing the expected vs. received output.
Assertion vs. Broken Flow
The runner matches any message that reaches Output, whichever branch of the flow produced it. A message emitted by a Catch node or any other error path is compared against Expected exactly like one from the success path, so error handling can make a case pass or fail. It only changes the outcome when it routes nothing to Output at all, in which case the case times out.
This makes error paths testable in their own right: give the failure branch a distinct Expected value and assert on it directly.
Return a message on both the success and failure branches (not just on success). This way:
Mismatch = the assertion ran and disagreed with your expected value
Timeout = nothing arrived at all (flow is broken or wiring is wrong)
Match Semantics
Matching between Expected and the message arriving at Output is partial. The actual message only needs to contain the keys and values listed in Expected; it does not need be an exact match.
Because matching is partial, Expected only needs to name the fields that matter to the assertion. You do not have to reconstruct the whole message shape.
What to include in Expected:
Only the fields your test actually validates
Extra fields in the actual message are ignored (they won't cause test failure)
What NOT to include in Expected:
_msgid— This is generated at runtime and will not match between test runs_contextual— This is auto-populated and should not be asserted againstAny other auto-generated or dynamic fields specific to your flow environment
Type Matching (Important):
String
"1"does not match number1String
"true"does not match booleantruenulldoes not matchundefinedNo type coercion is performed — types must match exactly
For substring assertions, word boundaries are respected (e.g.,
"hello"does not match inside"hello world")
Why Not Use an Inject Node
For HTTP routes, an Inject node does not create the real HTTP response object that an HTTP Response node needs, so Inject nodes cannot reach an HTTP Response terminal. Depending on the runtime path, the flow may warn that there is no response object to send or otherwise fail when the HTTP Response node tries to use an incomplete response object.
A Test node avoids this. Point its Output at the last meaningful node before the HTTP Response node — for example, the Function node that shapes the response body — and assert against that node instead.
Example: Asserting on an AI Generate Node
This example checks how AI Generate node behaves when its Tools option is set to None, so the model is given no callable tools and must answer from its own knowledge.
Flow Structure
The main chain runs left to right, ending at Log 3 — the node the Test node watches:
Test node → AI Generate → Log 2 → Function → Log 3
A separate error-handling branch runs only if Function throws. It is not connected by a wire; a group-scoped Catch node listens for errors raised anywhere inside the group:
Catch: group → Log 5 → HTTP Response
Test node
contextual-test
Injects the case message and watches Log 3
AI Generate
ai-generate
Sends the prompt to the model with no tools available
Log 2
log-tap
Taps the raw model response for the debug sidebar
Function
function
Converts the pass/fail decision into a plain payload value
Log 3
log-tap
The case's Output node
Catch: group
catch
Catches errors thrown inside the group
Log 5
log-tap
Captures the full msg, including error details
HTTP Response
http response
Terminal node for the error branch
Input
The case's Message is emitted from the Test node's output and enters the flow at the AI Generate node:
AI Generate Configuration
The AI Generate node reads its prompt from msg.payload and calls the open-ai route. Its Tools selector is set to None, so the model cannot call out to any tool and can only respond from its own training.
route
open-ai
tools
None selected
outputProperty
payload
responseProperty
_response
retries
3
Assertion Logic
Log 2 taps the raw model response for the debug sidebar, then hands it to Function, which turns the pass/fail decision for this case into a plain payload value:
Regardless of outcome, msg is still returned, so Log 3 always receives something to check Expected against — including on the failure branch that also throws to the Catch node.
Output
Log 3 is the node named as this case's Output. The runner waits up to the case timeout for a message to arrive there and checks it against Expected:
Output
Log 3
Expected
{ "payload": "success" }
Timeout
50000 ms
The case passes only when Function's regex finds the word "paris" somewhere in the model's answer within 50 seconds. It fails on timeout or on any other payload value.
Error Path
This branch is separate from the Test node's pass/fail check. It is the group's real error-handling path — what a live HTTP caller would receive if Function's assertion throws, independent of whatever the Test node itself reports.
Catch: group
Configured with scope-select set to group and uncaught set to false. Listens for any node.error() thrown by a node inside the same canvas group — in practice, only Function's failure branch.
Log 5
A Log Tap set to capture the full msg rather than only msg.payload (unlike Log 2 and Log 3), at debug level, sent to the sidebar only. Logging the whole message preserves the error details the Catch node attached.
HTTP Response
The terminal node for this branch. Its Status Code field and Headers list are both left blank.
Ignore errors handled by other Catch nodes is unchecked on the Catch node, so it fires even if a broader, tab-level Catch node already handled the same error. With only one Catch node in this group that is not currently a problem, but it is worth keeping in mind if a tab-level Catch node is added later.
How the Blank Status Code Behaves
The platform resolves the HTTP response status in this order:
If the node's own Status Code field is non-empty, that value wins and any msg.statusCode is ignored.
If the node's Status Code field is blank, the platform falls back to msg.statusCode from the incoming message.
If neither is set, the response defaults to 200.
In this example the node's field is blank, and nothing upstream (Function, Catch, or Log 5) sets msg.statusCode, so the error branch returns a plain 200 OK.
Warning: A caller hitting this route cannot tell from the HTTP status alone that the assertion failed; they would have to inspect the response body. Setting msg.statusCode = 500 in Function's failure branch, or configuring an explicit status code on the HTTP Response node, makes the failure visible over HTTP.
Last updated
Was this helpful?