---
sidebar_position: 4
title: Workflow Engine
---

# Workflow Engine

Zibby's workflow engine is a **graph-based orchestration system for AI agents**, inspired by [LangGraph](https://github.com/langchain-ai/langgraph) but designed to be **agent-agnostic**. The same workflow definition runs identically on Cursor, Claude, or Codex — you switch agents with a single flag, not by rewriting your pipeline.

## Core Concepts

### Graph = Nodes + Edges + State

A workflow is a directed graph where:

- **Nodes** — individual steps (AI calls, data transformations, tool usage)
- **Edges** — connections between nodes (linear or conditional)
- **State** — a shared key-value store that flows through the graph; every node reads from and writes to it

```
┌──────────┐     ┌──────────────┐     ┌─────────────────┐
│ preflight│────▶│ execute_live  │────▶│ generate_script  │───▶ END
└──────────┘     └──────────────┘     └─────────────────┘
                        │
                        ▼ (conditional)
                       END
                 (no actions recorded)
```

### Agent-Agnostic Design

The workflow framework never calls a specific AI provider directly. Instead, it uses a **Strategy Pattern**:

1. Each agent (Cursor, Claude, Codex) implements the `AgentStrategy` base class
2. The `invokeAgent()` function selects the right strategy at runtime based on your config
3. Nodes call `invokeAgent()` — they never know which provider executes the prompt

This means:
- Workflows are **portable** across agents
- Skills (MCP tools) are resolved per-agent automatically
- You can override the model per-node in config without touching workflow code

```javascript
// .zibby.config.js — switch agent with one line
export default {
  agent: {
    cursor: { model: 'auto' },
    // claude: { model: 'sonnet-4.6' },
    // codex: { model: 'gpt-5.2-codex' },
  }
};
```

### Three Supported Agents

| Agent | SDK | MCP Integration | Structured Output |
|---|---|---|---|
| **Cursor** | cursor-agent CLI / API | MCP servers via `~/.cursor/mcp.json` | JSON extraction from response |
| **Claude** | `@anthropic-ai/claude-agent-sdk` | Native MCP tool_use with `allowedTools` | Zod schema → SDK structured output |
| **Codex** | `@openai/codex-sdk` | `mcp_servers` config in SDK | Zod → JSON Schema via `outputSchema` |

All three implement the same interface:

```javascript
class AgentStrategy {
  canHandle(context)              // Is this agent available?
  invoke(prompt, options)         // Execute prompt with tools + schema
  getName()                       // 'cursor' | 'claude' | 'codex'
}
```

## Building a Workflow

### The Built-in Default: Browser Test Automation

When you run `zibby test test.txt`, the CLI uses the built-in `BrowserTestAutomationAgent` workflow — no init or config needed:

```javascript
import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
import { preflightNode, executeLiveNode, generateScriptNode } from './nodes/index.mjs';

export class BrowserTestAutomationAgent extends WorkflowAgent {
  buildGraph() {
    const graph = new WorkflowGraph();

    graph.addNode('preflight', preflightNode);
    graph.addNode('execute_live', executeLiveNode);
    graph.addNode('generate_script', generateScriptNode);

    graph.setEntryPoint('preflight');
    graph.addEdge('preflight', 'execute_live');

    graph.addConditionalEdges('execute_live', (state) => {
      const result = state.execute_live;
      const hasExecution = (result?.steps?.length > 0) || (result?.actions?.length > 0);
      return hasExecution ? 'generate_script' : 'END';
    });

    graph.addEdge('generate_script', 'END');
    return graph;
  }

  async onComplete(result) {
    // Save artifacts, push to memory, etc.
  }
}
```

**What each node does:**

| Node | Type | Skills | Purpose |
|---|---|---|---|
| `preflight` | Prompt-only (no tools) | — | Reads test spec, extracts title + assertion checklist via LLM |
| `execute_live` | Tool-using | Browser, Memory | AI drives a real browser, fills forms, clicks, captures selectors |
| `generate_script` | Prompt-only | — | Converts recorded actions into a reusable Playwright `.spec.js` |

### Custom Workflows via `zibby init`

Scaffold a customizable workflow into your project:

```bash
zibby init --agent cursor
```

This creates:

```
.zibby.config.js           # Project configuration
.zibby/
├── graph.js                # Workflow graph (nodes + edges)
├── nodes/
│   ├── preflight.js        # Extract title + assertions
│   ├── execute-live.js     # AI drives browser
│   └── generate-script.js  # Generate Playwright script
└── result-handler.js       # Post-execution artifact saving
```

You can then modify any node, add new nodes, or change the graph structure.

## WorkflowGraph API

### Creating a Graph

```javascript
import { WorkflowGraph, Node } from '@zibby/core';

const graph = new WorkflowGraph({
  stateSchema: MyZodSchema,     // Optional: Zod schema for state validation
  middleware: [myMiddleware],    // Optional: middleware functions
});
```

### Adding Nodes

```javascript
// Simple node with prompt function + output schema
graph.addNode('my_node', {
  name: 'my_node',
  prompt: (state) => `Analyze: ${state.testSpec}`,
  outputSchema: z.object({
    title: z.string(),
    items: z.array(z.string()),
  }),
});

// Node with skills (MCP tools)
graph.addNode('browser_step', {
  name: 'browser_step',
  skills: [SKILLS.BROWSER, SKILLS.MEMORY],
  prompt: (state) => `Execute: ${state.testSpec}`,
  outputSchema: ExecutionSchema,
  timeout: 600000,
});

// Node with custom execute (no LLM call)
graph.addNode('transform', {
  name: 'transform',
  _isCustomCode: true,
  execute: async (context) => {
    const data = context.state.get('raw_data');
    return { cleaned: data.trim(), wordCount: data.split(' ').length };
  },
  outputSchema: TransformSchema,
});
```

### Wiring Edges

```javascript
// Linear flow
graph.setEntryPoint('step_a');
graph.addEdge('step_a', 'step_b');
graph.addEdge('step_b', 'step_c');
graph.addEdge('step_c', 'END');

// Conditional branching
graph.addConditionalEdges('step_b', (state) => {
  return state.step_b.success ? 'step_c' : 'error_handler';
});
```

### Running the Graph

```javascript
const result = await graph.run(agent, {
  testSpec: 'Go to example.com and verify the title',
  cwd: process.cwd(),
  config: { agent: { cursor: { model: 'auto' } } },
});

console.log(result.success);        // true
console.log(result.state.preflight); // { title: '...', assertions: [...] }
console.log(result.executionLog);    // [{ node, success, duration }, ...]
```

## Node Anatomy

Every node has three key parts:

### 1. Prompt

A function or string template that receives the current state and produces the LLM prompt:

```javascript
prompt: (state) => `
  Test spec: ${state.testSpec}
  Previous results: ${JSON.stringify(state.preflight)}
  
  Execute the test and return structured results.
`
```

Nodes can also use Handlebars templates when configured with a `prompt` option:

```javascript
graph.addNode('my_node', nodeImpl, {
  prompt: `Analyze {{testSpec}} and produce {{outputFormat}} results`
});
```

### 2. Output Schema (Zod)

Every node declares a Zod schema for its output. The framework validates the AI response against it:

```javascript
import { z } from '@zibby/core';

const MyOutputSchema = z.object({
  title: z.string().describe('Concise test title'),
  assertions: z.array(z.object({
    description: z.string(),
    expected: z.string(),
  })),
});

export const myNode = {
  name: 'my_node',
  outputSchema: MyOutputSchema,
  prompt: (state) => '...',
};
```

The schema serves three purposes:
- **Runtime validation** — the LLM response is parsed and validated against the schema
- **Structured output** — Claude and Codex agents use the schema for native structured output (no JSON extraction needed)
- **State contract** — downstream nodes know exactly what shape to expect from upstream output

### 3. Skills

Skills declare what MCP tools a node needs. The framework resolves the right MCP server per-agent:

```javascript
import { SKILLS } from '@zibby/core';

export const executeLiveNode = {
  name: 'execute_live',
  skills: [SKILLS.BROWSER, SKILLS.MEMORY],
  // ...
};
```

Available built-in skills: `BROWSER`, `JIRA`, `GITHUB`, `SLACK`, `MEMORY`.

## State Management

State is a shared key-value store managed by `WorkflowState`. Each node's output is automatically stored under its name:

```javascript
// After preflight runs:
state.get('preflight')  // → { title: '...', assertions: [...] }

// After execute_live runs:
state.get('execute_live')  // → { success: true, steps: [...], actions: [...] }
```

Built-in state keys:

| Key | Type | Description |
|---|---|---|
| `messages` | `Array` | Accumulated messages |
| `errors` | `Array` | Error records `{ node, error }` |
| `artifacts` | `Object` | Generated artifacts |
| `config` | `Object` | Resolved `.zibby.config.js` |
| `agentType` | `string` | Active agent (`cursor`, `claude`, `codex`) |
| `sessionPath` | `string` | Path to session output directory |
| `context` | `Object` | Loaded context files (CONTEXT.md, AGENTS.md, env) |
| `testSpec` | `string` | The test specification text |
| `<node_name>` | `Object` | Each node's validated output |

State supports history and rollback:

```javascript
state.set('key', 'value');
state.update({ a: 1, b: 2 });
state.append('errors', { node: 'x', error: 'failed' });
state.rollback();  // Undo last mutation
```

## Graph Compiler

Zibby includes a **graph compiler** that takes a serialized JSON graph (from the dashboard's visual editor) and compiles it into an executable `WorkflowGraph`:

```javascript
import { compileGraph, validateGraphConfig } from '@zibby/core';

// Validate before compiling
const { valid, errors } = validateGraphConfig(graphJson);

// Compile JSON → executable graph
const graph = compileGraph(graphJson, {
  stateSchema: MySchema,
  middleware: [memoryMiddleware],
});

await graph.run(agent, initialState);
```

The compiler handles:
- **Node resolution** — maps node types to registered implementations
- **Decision nodes** — collapses visual decision diamonds into `addConditionalEdges`
- **Custom code** — safely compiles inline JavaScript from the visual editor
- **Tool resolution** — wires MCP tool permissions per node
- **Entry point detection** — finds the node with no incoming edges

## Middleware

Middleware wraps every node execution, enabling cross-cutting concerns:

```javascript
const graph = new WorkflowGraph({
  middleware: [
    async (nodeName, next, stateValues, state) => {
      console.log(`Starting: ${nodeName}`);
      const result = await next();
      console.log(`Completed: ${nodeName} in ${result.duration}ms`);
      return result;
    }
  ]
});
```

Skills can also provide middleware. For example, the Memory skill injects a middleware that loads test history before each node and persists insights after:

```javascript
// Automatically injected when a node declares skills: [SKILLS.MEMORY]
const memoryMiddleware = await memorySkill.middleware();
```

## Skill Adaptation per Agent

Skills are resolved differently depending on which agent is active:

| Agent | How Skills (MCP servers) Are Wired |
|---|---|
| **Cursor** | Written to `~/.cursor/mcp.json` — Cursor IDE manages the MCP lifecycle |
| **Claude** | Passed as `mcpServers` config to the Claude Agent SDK — native MCP support |
| **Codex** | Passed as `mcp_servers` config to the Codex SDK — SDK manages MCP processes |

The skill's `resolve()` function returns a platform-neutral server config:

```javascript
{
  command: 'node',
  args: ['/path/to/mcp-browser.js', '--save-video=1280x720', '--viewport-size=1280x720', '--output-dir=./output'],
  env: { SOME_KEY: '...' }
}
```

Each agent strategy translates this into its native MCP format. Node code never changes.

## Context Loading

The workflow engine auto-discovers context files that are prepended to prompts:

```javascript
// .zibby.config.js
export default {
  context: {
    filenames: ['CONTEXT.md', 'AGENTS.md'],
    discovery: {
      env: `env-${process.env.ENV || 'local'}.js`,
    }
  }
};
```

Context files cascade from the project root down to the spec's directory. This lets you provide domain knowledge (login credentials, app structure, known quirks) that the AI uses during execution.

## Per-Node Model Override

Override the AI model for specific nodes without changing the workflow:

```javascript
// .zibby.config.js
export default {
  agent: {
    cursor: { model: 'auto' },
  },
  models: {
    default: 'auto',
    execute_live: 'claude-opus-4',    // Use a more capable model for browser execution
    preflight: 'claude-sonnet-4.6',   // Faster model for analysis
  }
};
```

Model resolution priority: node override → `models.default` → agent block model → `'auto'`.

## Workflow Templates

`@zibby/core` ships two built-in workflow templates:

### Browser Test Automation (`browser-test-automation`)

The default workflow for `zibby test`:

```
preflight → execute_live → generate_script → END
```

Three nodes: extract spec, drive browser, generate Playwright script.

### Code Analysis (`code-analysis`)

Used by the cloud analysis pipeline:

```
setup → analyze_ticket → generate_code → generate_test_cases → finalize → END
```

Six nodes: clone repo, analyze Jira ticket, generate code changes, write test cases, compile report.

## Creating Custom Skills

### MCP Skill (wraps an external MCP server)

```javascript
import { skill } from '@zibby/skills';

export const linear = skill('linear', {
  description: 'Linear project management',
  serverName: 'linear',
  allowedTools: ['mcp__linear__*'],
  envKeys: ['LINEAR_API_KEY'],
  resolve() {
    if (!process.env.LINEAR_API_KEY) return null;
    return {
      command: 'npx',
      args: ['-y', '@anthropic/linear-mcp-server'],
      env: { LINEAR_API_KEY: process.env.LINEAR_API_KEY }
    };
  }
});
```

### Function Skill (single tool, no MCP server)

```javascript
import { skill } from '@zibby/skills';

export const add = skill('add', {
  description: 'Add two numbers',
  input: { a: 'number', b: 'number' },
  handler: async ({ a, b }) => ({ result: a + b })
});
```

Function skills are automatically bridged to MCP at runtime — the framework spawns a lightweight MCP server that delegates to your handler.

## End-to-End Example: Custom Workflow

```javascript
import { WorkflowGraph, SKILLS } from '@zibby/core';
import { z } from '@zibby/core';

// 1. Define output schemas
const AnalysisSchema = z.object({
  summary: z.string(),
  testCases: z.array(z.object({
    name: z.string(),
    steps: z.array(z.string()),
  })),
});

const ExecutionSchema = z.object({
  success: z.boolean(),
  steps: z.array(z.string()),
  actions: z.array(z.object({
    type: z.string(),
    description: z.string(),
  })),
  browserClosed: z.boolean(),
});

// 2. Define nodes
const analyzeNode = {
  name: 'analyze',
  prompt: (state) => `Analyze this requirement and produce test cases:\n${state.testSpec}`,
  outputSchema: AnalysisSchema,
};

const executeNode = {
  name: 'execute',
  skills: [SKILLS.BROWSER, SKILLS.MEMORY],
  prompt: (state) => `Execute test "${state.analyze.testCases[0].name}":\n${state.analyze.testCases[0].steps.join('\n')}`,
  outputSchema: ExecutionSchema,
  timeout: 600000,
};

// 3. Build graph
const graph = new WorkflowGraph();
graph.addNode('analyze', analyzeNode);
graph.addNode('execute', executeNode);
graph.setEntryPoint('analyze');
graph.addEdge('analyze', 'execute');
graph.addEdge('execute', 'END');

// 4. Run
const result = await graph.run(agent, {
  testSpec: 'Verify the user can log in and see the dashboard',
  cwd: process.cwd(),
});
```

## Workflow CLI Commands

```bash
# Run a specific workflow (if you have multiple defined)
zibby test test.txt --workflow QuickSmokeWorkflow

# Run a single node (for debugging)
zibby test test.txt --node execute_live --session last

# Upload/download workflow graphs to/from Zibby Cloud
zibby workflow list
zibby workflow download --type run_test
zibby workflow upload --type analysis --file .zibby/workflow-analysis.json
```
