Cori
Guides

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:

  1. Static validationcori check validates the manifest, step file structure, and declared tools without executing anything.
  2. Unit testsdeno test tests for code steps, using captured input/output fixtures.

cori check — static validation

cori check ./my_workflow

Run 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.

deno.json
{
  "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 code steps with an import map of exactly @cori-do/sdk + zod and network limited to registry.npmjs.org,esm.sh,jsr.io. The test task uses the same allow-net allowlist, so a code step that legitimately imports an npm:/jsr:/esm.sh package 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-check skips type-checking at test time (the SDK types a step's run return loosely, so a strict check trips on field access in assertions). The test still executes the real run logic.
  • Pin versions to current. Check with npm view @cori-do/sdk version. zod must 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.

tests/03_check_gpsr.test.ts
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 test

Why 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 output

Load 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);

On this page