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:
| Aspect | v0.2.0 (Old) | v0.3.0 (New) |
|---|---|---|
| Orchestration | YAML flows, explicit edges | createWorkflow<TState>(), signal-based |
| Execution | Sequential DAG walk | Parallel signal dispatch |
| Agent Definition | nodes: [{ type: claude.agent }] | agent({ prompt, activateOn, emits }) |
| Expressions | JSONata | {{ state.x }} template syntax |
| State | Passive object | Typed state with updates field |
| Recording | Snapshots per node | Event-sourced signal log |
| Testing | Fixtures | Vitest 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: analyzer2. Install Updated Packages
bun add @open-harness/core @open-harness/vitest3. 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 Event | v0.3.0 Signal |
|---|---|
flow:start | workflow:start |
flow:complete | workflow:end |
node:start | agent:activated |
node:complete | agent:completed |
node:skipped | agent:skipped |
agent:text | text:complete |
agent:text:delta | text:delta |
agent:tool | tool:call, tool:result |
state:patch | state:\{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
activateOnsignal subscriptions - WebSocket transport - Access signals directly from result
- Node registry - Agents are defined inline
- Loop edges - Use
whenguards 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@latestType 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?
- Quickstart - Fresh start with v0.3.0
- Signal System - Understanding signals
- Troubleshooting - Common errors