Test a workflow
Write Deno tests for code steps using captured fixtures, and use cori check for static validation.
Two layers of testing
Cori workflows have two testing surfaces:
- Static validation —
cori checkvalidates the manifest, step file structure, and declared tools without executing anything. - Unit tests —
deno testtests forcodesteps, using captured input/output fixtures.
cori check — static validation
cori check ./my_workflowRun this before every commit. It catches:
- Missing or malformed manifest fields
- Step files that don't export the right shape (
input,output, default step) - Declared tools or MCP servers that can't be found
- Type mismatches between step outputs and the next step's inputs
Test with Deno, not Node
Cori's runtime is Deno: cori run executes every code step inside a Deno sandbox, with a fixed import map and a tight permission set. So Deno is guaranteed present wherever Cori is installed, and Node/npm is not needed.
Test your steps with deno test rather than a Node test runner. The point is resolution parity: a Deno test resolves imports with the same import map the runtime uses, so a passing test proves the step's imports actually load under cori run. A Node/vitest test would resolve packages from node_modules — a different mechanism — and can pass while the step fails at runtime.
Don't add a package.json, node_modules, or vitest harness to a workflow. It introduces a second toolchain that resolves imports differently from the runtime, so a green test stops being a faithful proxy for a green run.
deno.json — the workflow's import map
Add a deno.json at the workflow root. Its import map mirrors the runtime's, so the @cori-do/sdk and zod imports in every step and test resolve exactly as they will under cori run. There's no npm install and no node_modules — Deno fetches and caches npm: modules itself on first run.
{
"imports": {
"@cori-do/sdk": "npm:@cori-do/sdk@^0.2.4",
"zod": "npm:zod@^4.4.3"
},
"tasks": {
"test": "deno test --no-check --allow-read --allow-env --allow-net=registry.npmjs.org,esm.sh,jsr.io tests/"
}
}- Mirror the runtime's resolution. The runner runs
codesteps with an import map of exactly@cori-do/sdk+zodand network limited toregistry.npmjs.org,esm.sh,jsr.io. Thetesttask uses the same allow-net allowlist, so acodestep that legitimately imports annpm:/jsr:/esm.shpackage resolves in tests just as it will at runtime — and a bad bare import fails the test with the same error the runtime would raise. --no-checkskips type-checking at test time (the SDK types a step'srunreturn loosely, so a strict check trips on field access in assertions). The test still executes the realrunlogic.- Pin versions to current. Check with
npm view @cori-do/sdk version.zodmust satisfy the SDK's peer range; a mismatched major is a silent break.
Unit testing code steps
code steps are pure functions — they're the easiest to test. Write a Deno.test that calls the step's run function with a fixture input and asserts the output.
import { assertEquals } from 'jsr:@std/assert';
import step, { input, output } from '../steps/03_check_gpsr.ts';
Deno.test('03_check_gpsr: filters rows missing manufacturer', () => {
const testInput = input.parse({
rows: [
{ id: '1', name: 'Widget', manufacturer: 'ACME' },
{ id: '2', name: 'Gadget', manufacturer: '' },
{ id: '3', name: 'Doohickey' },
],
});
const result = output.parse(step.run({ input: testInput }));
assertEquals(result.valid_rows.length, 1);
assertEquals(result.valid_rows[0].id, '1');
assertEquals(result.issues.length, 2);
});Note the Deno idioms: an explicit .ts extension on the step import, and jsr:@std/assert for assertions (assertEquals, assertMatch, …).
Run tests from inside the workflow directory:
cd ./my_workflow
deno task testWhy cli and mcp_tool steps usually don't need unit tests
cli and mcp_tool steps are thin wrappers around external tools. Their behavior is the external tool's responsibility. Testing them would be integration-testing the tool, not the step.
Focus unit tests on code steps, where your logic lives. For llm steps, unit testing the prompt is rarely valuable — test the output parsing/schema validation instead.
Captured fixtures (real inputs and outputs from a conversation) are better test inputs than invented ones. When an agent authors a workflow, it can generate test fixtures directly from the conversation artifacts.
Suggested test structure
my_workflow/
deno.json # import map + `deno task test`
tests/
03_check_gpsr.test.ts # test for each code step
fixtures/
03_check_gpsr_input.json # captured real input
03_check_gpsr_output.json # expected outputLoad fixtures in tests with a JSON import attribute:
import fixture from './fixtures/03_check_gpsr_input.json' with { type: 'json' };
const testInput = input.parse(fixture);
