Open Harness
Learn

Migration Guide

Migrate from v0.2.0 to v0.3.0

Migration Guide

This guide helps you migrate from Open Harness v0.2.0 (YAML flows) to v0.3.0 (signal-based reactive architecture).

Overview of Changes

v0.3.0 introduces a fundamentally new architecture:

Aspectv0.2.0 (Old)v0.3.0 (New)
OrchestrationYAML flows, explicit edgescreateWorkflow<TState>(), signal-based
ExecutionSequential DAG walkParallel signal dispatch
Agent Definitionnodes: [{ type: claude.agent }]agent({ prompt, activateOn, emits })
ExpressionsJSONata{{ state.x }} template syntax
StatePassive objectTyped state with updates field
RecordingSnapshots per nodeEvent-sourced signal log
TestingFixturesVitest matchers + signal assertions

Step-by-Step Migration

1. Remove YAML Flow Files

Delete any .yaml or .yml flow definitions:

# OLD: flow.yaml - DELETE THIS
name: my-flow
nodes:
  - id: analyzer
    type: claude.agent
    config:
      prompt: "Analyze the input"
edges:
  - from: start
    to: analyzer

2. Install Updated Packages

bun add @open-harness/core @open-harness/vitest

3. Define State Type

Create a TypeScript type for your workflow state:

// OLD: No explicit state type
// NEW: Explicit state type
type State = {
  input: string;
  analysis: string | null;
  review: string | null;
};

4. Create Workflow with Typed State

// OLD
import { createFlow } from "@open-harness/core";
const flow = await createFlow("./flow.yaml");

// NEW
import { createWorkflow } from "@open-harness/core";

type State = { input: string; result: string | null };
const { agent, runReactive } = createWorkflow<State>();

5. Convert Nodes to Agents

// OLD: YAML node definition
// nodes:
//   - id: analyzer
//     type: claude.agent
//     config:
//       prompt: "Analyze: {{ $.input }}"

// NEW: TypeScript agent definition
const analyzer = agent({
  prompt: "Analyze: {{ state.input }}",
  activateOn: ["workflow:start"],
  emits: ["analysis:complete"],
  updates: "result",
});

6. Replace Edges with Signals

Edges are now implicit through signal subscriptions:

// OLD: Explicit edges in YAML
// edges:
//   - from: analyzer
//     to: reviewer

// NEW: Signal-based activation
const analyzer = agent({
  prompt: "Analyze the input",
  activateOn: ["workflow:start"],
  emits: ["analysis:complete"],
});

const reviewer = agent({
  prompt: "Review: {{ state.analysis }}",
  activateOn: ["analysis:complete"],  // Triggered by analyzer
  emits: ["review:complete"],
});

7. Update Expression Syntax

// OLD: JSONata expressions
// prompt: "Process: {{ $.data.items[0].name }}"

// NEW: Handlebars-style templates
prompt: "Process: {{ state.data }}"

State access is now via state.key instead of $.key.

8. Replace run() with runReactive()

// OLD
import { run } from "@open-harness/core";
const result = await run(flow, { input: "Hello" });

// NEW
import { createWorkflow, ClaudeHarness } from "@open-harness/core";

const { agent, runReactive } = createWorkflow<State>();

const result = await runReactive({
  agents: { analyzer, reviewer },
  state: { input: "Hello", result: null },
  harness: new ClaudeHarness(),
  endWhen: (state) => state.result !== null,
});

9. Update Event Handling

// OLD: Event types
runtime.onEvent((event) => {
  if (event.type === "node:complete") {
    console.log(event.nodeId, event.output);
  }
});

// NEW: Signal types
const result = await runReactive({ /* ... */ });
for (const signal of result.signals) {
  if (signal.name === "agent:completed") {
    console.log(signal.payload.agent, signal.payload.output);
  }
}

10. Update Tests

// OLD: Fixture-based testing
import { test } from "vitest";
import { runWithFixture } from "@open-harness/testing";

test("analyzer works", async () => {
  const result = await runWithFixture("analyzer-test", myFlow);
  expect(result.output).toBeDefined();
});

// NEW: Signal-based testing
import { test, expect } from "vitest";
import { toContainSignal, toHaveSignalsInOrder } from "@open-harness/vitest";

expect.extend({ toContainSignal, toHaveSignalsInOrder });

test("analyzer works", async () => {
  const result = await runReactive({
    agents: { analyzer },
    state: initialState,
    harness: new ClaudeHarness(),
  });

  expect(result.signals).toContainSignal("agent:completed");
  expect(result.state.result).toBeDefined();
});

Signal Mapping

Map old event types to new signal names:

v0.2.0 Eventv0.3.0 Signal
flow:startworkflow:start
flow:completeworkflow:end
node:startagent:activated
node:completeagent:completed
node:skippedagent:skipped
agent:texttext:complete
agent:text:deltatext:delta
agent:tooltool:call, tool:result
state:patchstate:\{key\}:changed

Conditional Execution

// OLD: When guards in YAML
// nodes:
//   - id: reviewer
//     when: "$.score > 0.8"

// NEW: When guards as functions
const reviewer = agent({
  prompt: "Review the content",
  activateOn: ["analysis:complete"],
  when: (state) => state.score > 0.8,
});

Recording & Replay

// OLD: Snapshot-based recording
const store = new FileFixtureStore("./fixtures");
await runWithRecording(flow, input, store);

// NEW: Signal-based recording
const store = new MemorySignalStore();

// Record
const result = await runReactive({
  agents: { analyzer },
  state: initialState,
  harness: new ClaudeHarness(),
  recording: { mode: "record", store },
});

// Replay
const replay = await runReactive({
  agents: { analyzer },
  state: initialState,
  harness: new ClaudeHarness(),
  recording: { mode: "replay", store, recordingId: result.recordingId },
});

Removed Features

The following v0.2.0 features are no longer available:

  • YAML flow definitions - Use TypeScript agent() calls
  • JSONata expressions - Use {{ state.key }} templates
  • Explicit edges - Use activateOn signal subscriptions
  • WebSocket transport - Access signals directly from result
  • Node registry - Agents are defined inline
  • Loop edges - Use when guards and state conditions

Common Migration Issues

"Cannot find module '@open-harness/core'"

Ensure you've updated to the latest packages:

bun add @open-harness/core@latest

Type errors with state

Make sure your state type matches the generic parameter:

type State = { input: string; result: string | null };
const { agent, runReactive } = createWorkflow<State>();

// State in runReactive must match State type
await runReactive({
  state: { input: "Hello", result: null }, // ✓ Matches State
});

Agents not activating

Check that your signal chain is complete:

// The emitter must declare emits
const analyzer = agent({
  emits: ["analysis:complete"], // Must declare this
});

// The subscriber activates on that signal
const reviewer = agent({
  activateOn: ["analysis:complete"], // Matches above
});

Need Help?

On this page