Workflow test suites
A workflow can carry saved test cases. Each case runs the workflow in test mode with its own input, can answer chosen nodes with a canned result instead of calling the real service, and checks node outputs against rules. Running the suite runs every enabled case and reports a result per case, in the builder or from a CI job with fluidgrids test, which exits non-zero when a case fails. A change that alters a node's output shows up as a failing case before the version goes live.
Workflow test suites are being rolled out workspace by workspace. If the builder has no Tests button, they are not enabled for your workspace yet, and the API answers FEATURE_DISABLED.
What a test case holds
| Field | What it does |
|---|---|
| Name and Description | The name is unique (ignoring case) among the workflow's cases. |
| Run this case with Run all | Turn it off to keep a case without running it in every suite. |
| The run should | Complete (the default) or Fail. A case that expects a failure passes when the run fails after the nodes it checks have completed. |
| Start on | The trigger node the run starts on. The workflow's entry node works when the workflow has exactly one. |
| Trigger input | The JSON the trigger receives, as when you run the workflow with input. |
| Connection mocks | Nodes that answer with a canned result instead of calling their connection (see Mocked connections). |
| Pinned outputs | Node outputs used instead of running the node: { "<nodeId>": { "data": { ... } } }, the same shape as the pinned data on the canvas. Use the canvas's pinned data copies the pins of the open version. |
| Expectations | Rules checked on a node's output as soon as that node completes (see Rules). |
Cases belong to the workflow, not to one version, so the same cases test every version you save. A case that names a node the tested version does not have is reported as UNKNOWN_NODE.
The Tests panel
In the visual builder, select Tests in the toolbar, next to Environments and Git.
- Cases lists the workflow's test cases with their last result. Open a case to edit it, Run to run only that case, or Delete to remove it. Past suite results keep their record of a deleted case.
- New test case opens the case editor. It checks every field the way the service does and shows a problem under the field it belongs to.
- Run all runs every enabled case against the version open on the canvas. While the suite runs the panel refreshes on its own; when it settles, Latest run shows each case as passed or failed, with its failures (code, node and message) and a link to the case's run. Earlier suite runs are listed below it.
Changing cases needs edit access to the workflow; running them needs permission to run it. A viewer sees the cases and their results.
Save a run as a test case
Open a finished run of a canvas workflow (completed or failed) from Runs (the Executions screen) and choose Save as test case. The new case replays the run's input, and every connection call the run made becomes a connection mock that answers with the output it returned. The built-in nodes run for real, so the case checks the workflow's own logic against the results your services gave that day. Add expectations to the case to say what must stay true.
Fields the workflow redacts in run data are saved redacted, and a private run cannot be saved as a case.
Mocked connections
A connection mock answers one node instead of the service the node calls:
[
{ "nodeId": "lookup_customer", "response": { "id": "cus_123", "plan": "pro" } },
{ "nodeId": "send_invoice", "operation": "create", "response": { "status": "sent" } },
{ "nodeId": "notify_slack", "error": { "message": "Slack is down", "errorType": "DEPENDENCY" } }
]
responsebecomes the node's whole output, so give it the shape the node really returns. Save as test case records that shape for you.errormakes the node fail with that message and error type (DEPENDENCYwhen you leave the type out). The node's error handling (retries, Continue, an error branch) applies, so you can test the failure paths too.operationlimits a mock to one operation of the node. A mock for the exact operation wins over one without an operation.- A mocked node never resolves its credentials and never calls its service. The node's state in the run records that a mock answered it (
usedMockin the API). - When a node has both pinned output and a mock, the pinned output wins.
Mocks apply to the nodes of the tested workflow. A sub-workflow that the run starts runs without them.
Rules
Expectations and the Assert node use the same rules. A rule has a path into the value it checks (leave it out to check the whole value), an operator, and the operand the operator needs:
| Operator | Passes when | Operand |
|---|---|---|
equals / notEquals | The value is (or is not) deeply equal to expected. Types must match: 1 is not "1". | expected |
contains / notContains | Text contains the text, a list has a matching element, or an object has every key and value given. | expected |
matches | Text matches the regular expression in expected. | expected |
jsonSchema | The value is valid against the JSON Schema in expected (draft-07, 2019-09 or 2020-12). | expected |
between | The value is a number from min to max, both included. | min, max (either may be left out) |
itemCount | The value is a list with min to max items. | min, max (either may be left out) |
exists / notExists | The value is present and not null (or is missing or null). | none |
- Paths are dotted:
customer.email,items.0.id, anditems[0]["odd.key"]for keys that contain dots. - A missing value fails every operator except
notExists. - Add a
messageto a rule to say what it guards; a failure shows it. - Patterns in
matchesand in JSON Schemas run on a linear-time regular-expression engine. Backreferences and lookarounds are not supported: a rule that uses them fails with a message saying so. - A node can have at most 50 rules, and a pattern at most 500 characters.
An expectation looks like this:
[
{ "nodeId": "quote", "rules": [
{ "path": "amount", "operator": "equals", "expected": 42 },
{ "path": "currency", "operator": "matches", "expected": "^[A-Z]{3}$", "message": "an ISO currency code" }
] },
{ "nodeId": "lookup_customer", "rules": [ { "path": "plan", "operator": "exists" } ] }
]
When a rule does not hold, the node fails with the code ASSERTION_FAILED, and with it the run. The node's error handling does not apply to this failure: it is the test's verdict. The node's output lists each rule with whether it held.
The Assert node
Assert is a built-in node that checks the data it receives, in any run, not only in test cases. Its settings:
- Rules (JSON array): the rules above. The node panel edits them as rows (Path, Operator, Expected, Message) with Add rule, or as JSON with Edit as JSON.
- When a rule does not hold: Fail the run (ASSERTION_FAILED) (the default), or Continue on the Fail output, which routes the data to the node's Fail output instead of failing.
The node's output is { passed, data, results }: whether every rule held, the incoming data unchanged, and one result per rule. It leaves on the Pass or Fail output. Its rules are fixed values, not expressions.
Results
A suite run is Passed when every case passed, Failed when any case failed, and Error when no case failed but at least one could not run. A case reports these codes:
| Code | The case | Meaning |
|---|---|---|
ASSERTION_FAILED | Failed | A rule did not hold. |
NODE_FAILED, NODE_TIMEOUT | Failed | A node failed or timed out in a case that expects the run to complete. |
UNEXPECTED_STATUS | Failed | The case expects the run to fail, but it completed. |
EXPECTED_NODE_NOT_RUN | Failed | A node with expectations did not complete. |
RUN_FAILED | Error | The run failed before any node failed, so the workflow never ran. |
UNKNOWN_NODE, TRIGGER_NODE_REQUIRED, RUN_NOT_STARTED | Error | The case could not start: it names a node the version lacks, it needs Start on, or the run was refused. |
RUN_CANCELLED, RUN_TERMINATED, RUN_MISSING, SUITE_TIMEOUT | Error | The run was stopped, removed, or did not finish within six hours. |
Every case runs as its own run in test mode, so it appears on the Executions screen with the rest, and you can open it to see each node's input and output.
Safety and limits
- Test cases only ever affect test runs. Mocks and pinned outputs never reach a production run: a run started by a webhook, a schedule or any other trigger calls the real services even when the workflow has test cases.
- Suite runs do not count toward your plan's monthly run quota. Instead, a workflow can have 50 test cases, and a workspace can have 5 suites running at once and start 60 suites an hour.
- A test case holds at most 256 KiB of JSON, 100 connection mocks, 100 pinned nodes and 100 nodes with expectations.
- Canvas workflows only: durable-function workflows cannot be tested this way yet.
Example: test a quote workflow
The workflow quote sets an amount, fetches an exchange rate over HTTP, and checks the rate with an Assert node:
Trigger Manually → Set (amount 42, currency EUR) → HTTP Request (exchange rate) → Assert (rate between 0.5 and 2)
Three cases cover it without calling the rate service:
| Case | Mock or pin | Checks | The run should |
|---|---|---|---|
| mocked connection | Mock the HTTP node: { "rate": 1.08 } | amount equals 42 on Set | Complete |
| pinned output | Pin the HTTP node: { "data": { "rate": 0.92 } } | passed equals true on Assert | Complete |
| connection outage | Mock the HTTP node with an error | currency equals "EUR" on Set | Fail |
Run all passes all three. Save a version that changes the amount to 41 and run the suite again: mocked connection fails with ASSERTION_FAILED on the Set node, and the other two still pass.
Run the suite in CI
fluidgrids test <workflowKey> runs the suite on the service, waits for it, prints a row per case and exits 0 when every case passed, 1 when a case failed or could not run, 2 when the token is refused, and 3 on a timeout. --junit writes a JUnit XML report your CI can display:
name: Workflow tests
on: [pull_request]
jobs:
workflow-tests:
runs-on: ubuntu-latest
steps:
- name: Run the quote workflow's test suite
run: npx --yes @fluidgrids/cli test quote --wf-version 1.4.0 --junit junit.xml
env:
FLUIDGRIDS_TOKEN: ${{ secrets.FLUIDGRIDS_TOKEN }}
FLUIDGRIDS_WORKSPACE_ID: ${{ vars.FLUIDGRIDS_WORKSPACE_ID }}
- name: Keep the report
if: always()
uses: actions/upload-artifact@v4
with:
name: workflow-tests
path: junit.xml
See CLI Reference: Workflow test suites for every option.