---
sidebar_position: 5
title: Custom Workflows
---

# Custom Workflows

Build, test, and deploy your own AI workflows using Zibby's graph-based framework. Custom workflows let you define multi-step AI pipelines that run locally or in Zibby Cloud, triggered via API or subdomain URL.

## Quick Start

```bash
# 1. Scaffold a new workflow
zibby g workflow ticket-triage

# 2. Test locally
zibby start ticket-triage

# 3. Deploy to cloud
zibby deploy ticket-triage --project <project-id>

# 4. Trigger via API
curl -X POST https://ticket-triage-6af9.workflows.zibby.app \
  -H "Authorization: Bearer $ZIBBY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": {"ticket": "BUG-123"}}'

# 5. Tail logs
zibby logs --workflow ticket-triage --project <project-id>
```

## Scaffolding

```bash
zibby g workflow <name>
```

If you omit the name, Zibby generates a random one (like Heroku app names).

This creates:

```
.zibby/workflows/<name>/
├── graph.mjs          # Workflow class (entry point)
├── nodes/
│   ├── index.mjs      # Barrel export
│   └── example.mjs    # Starter node with prompt + schema
└── workflow.json       # Manifest (metadata, triggers)
```

### Workflow Structure

**`graph.mjs`** — Defines the workflow class that extends `WorkflowAgent`:

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

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

    graph.addNode('example', exampleNode);
    graph.setEntryPoint('example');
    graph.addEdge('example', 'END');

    return graph;
  }

  async onComplete(result) {
    console.log(`Workflow complete — success: ${result.success !== false}`);
  }
}
```

**`nodes/example.mjs`** — Each node has a prompt function and a Zod output schema:

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

const ExampleOutputSchema = z.object({
  summary: z.string().describe('A short summary of the result'),
  status: z.enum(['ok', 'warn', 'error']).describe('Overall status'),
});

export const exampleNode = {
  name: 'example',
  prompt: (state) => `You are a helpful workflow node.

Input:
${JSON.stringify(state.input || {}, null, 2)}

Analyze the input and return a summary with a status.`,
  outputSchema: ExampleOutputSchema,
};
```

**`workflow.json`** — Manifest with metadata:

```json
{
  "name": "ticket-triage",
  "triggers": { "api": true }
}
```

## Adding Nodes

Create a new file in `nodes/` and wire it into the graph:

```javascript
// nodes/classify.mjs
import { z } from '@zibby/core';

const ClassifySchema = z.object({
  priority: z.enum(['critical', 'high', 'medium', 'low']),
  category: z.string(),
  assignTo: z.string().optional(),
});

export const classifyNode = {
  name: 'classify',
  prompt: (state) => `Given this ticket summary:
${state.example.summary}

Classify the priority, category, and suggested assignee.`,
  outputSchema: ClassifySchema,
};
```

Then add it to `graph.mjs`:

```javascript
import { classifyNode } from './nodes/classify.mjs';

// In buildGraph():
graph.addNode('classify', classifyNode);
graph.addEdge('example', 'classify');  // instead of example → END
graph.addEdge('classify', 'END');
```

### Conditional Edges

Route to different nodes based on output:

```javascript
graph.addConditionalEdges('classify', (state) => {
  return state.classify.priority === 'critical' ? 'escalate' : 'notify';
});
```

## Local Development

### Start a dev server

```bash
zibby start ticket-triage
zibby start ticket-triage --port 3850
```

This starts a local HTTP server that loads your workflow and exposes a trigger endpoint:

```bash
curl -X POST http://localhost:3848/trigger \
  -H "Content-Type: application/json" \
  -d '{"input": {"ticket": "BUG-456"}}'
```

The dev server uses your local `.zibby.config.mjs` for agent configuration (model, API keys, etc.) — the same config used by `zibby test`.

## Deploying to Cloud

### Prerequisites

1. **Authenticated**: Run `zibby login` (or set `ZIBBY_API_KEY`)
2. **Project**: Have a project ID (run `zibby list` to see yours)

### Deploy

```bash
zibby deploy ticket-triage --project <project-id>
```

This:
1. Loads and serializes the workflow graph
2. Bundles all source files (`.mjs`, `.js`, `.json`)
3. Uploads to Zibby Cloud
4. Registers a unique subdomain

Output:

```
  Workflow "ticket-triage" deployed to version 1

  Trigger URL (API):
    POST https://api-prod.zibby.app/projects/<id>/workflows/ticket-triage/trigger

  Trigger URL (subdomain):
    POST https://ticket-triage-6af9.workflows.zibby.app

  Tail logs:
    zibby logs <jobId> --project <id>
```

### Subdomain URLs

Each deployed workflow gets a globally unique subdomain:

```
https://<workflow-name>-<hash>.workflows.zibby.app
```

The hash is a short (4-char) deterministic suffix derived from your project ID, ensuring uniqueness across all projects.

### Authentication

Both trigger URLs require authentication via the `Authorization` header:

- **JWT token** — from `zibby login` session
- **Personal Access Token (PAT)** — from your project settings (`zby_xxx`)

```bash
curl -X POST https://ticket-triage-6af9.workflows.zibby.app \
  -H "Authorization: Bearer zby_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"input": {"ticket": "BUG-789"}}'
```

### How Cloud Execution Works

When triggered, the workflow runs in an isolated ECS Fargate container:

1. Lambda receives the trigger request
2. Workflow sources are loaded from DynamoDB and uploaded to S3
3. A Fargate task is launched with the workflow code
4. The container downloads sources, rebuilds the graph, and executes it
5. Agent configuration (model, API keys) comes from your project settings

Each run is fully isolated — no shared state between runs.

## Cloning Repositories

Custom workflows can clone your project's configured repositories using the `cloneRepo()` helper:

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

// In a node's preProcess function
const repoPaths = await cloneRepo();
// Returns: { 'myorg/backend': '/workspace/repos/myorg-backend', ... }
```

This gives your workflow access to your actual codebase for analysis, testing, or deployment tasks.

**[See the full Cloning Repositories guide →](./cloning-repositories.md)**

## Tailing Logs

### Tail a specific job

The trigger API returns a `jobId`. Use it to tail logs:

```bash
zibby logs <jobId> --project <project-id>
```

### Tail the latest run

```bash
zibby logs --workflow ticket-triage --project <project-id>
```

This lists recent runs and automatically tails the latest one.

### All runs (interleaved)

```bash
zibby logs --workflow ticket-triage --all --project <project-id>
```

Shows logs from all past runs of the workflow, sorted chronologically with job ID separators:

```
  ── wfj-1713157331-a3 ──
2026-04-15 14:02:11  🚀 Starting ticket-triage workflow...
2026-04-15 14:02:18  ✅ Workflow complete

  ── wfj-1713157522-b7 ──
2026-04-15 14:05:22  🚀 Starting ticket-triage workflow...
2026-04-15 14:05:30  Running node: example
```

### Options

| Flag | Description |
|---|---|
| `--project <id>` | Project ID (or `ZIBBY_PROJECT_ID` env) |
| `--workflow <name>` | Workflow name (tails latest run) |
| `--all` | Interleaved logs from all runs (requires `--workflow`) |
| `--no-follow` | Fetch logs once, don't stream |
| `--lines <n>` | Max lines per fetch (default: 200) |

## Agent Configuration

### Local

Local execution uses your `.zibby.config.mjs`:

```javascript
export default {
  agent: {
    cursor: { model: 'auto' },
    // claude: { model: 'sonnet-4.6' },
  },
};
```

API keys come from environment variables (`CURSOR_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`).

### Cloud

Cloud execution uses your **project settings** (configured in the Zibby dashboard):

- **AI Agent** — which agent to use (Cursor, Claude, Codex, Gemini)
- **Model** — model override
- **API Key** — stored encrypted, injected into the container at runtime

No config files are needed in the cloud — everything is read from project settings.

## Self-Hosting

You can run workflows on your own infrastructure without Zibby Cloud. Your server just needs:

1. `@zibby/core` and `@zibby/cli` npm packages
2. Environment variables for agent configuration:

```bash
AGENT_TYPE=cursor          # or claude, codex, gemini
MODEL=auto
CURSOR_API_KEY=sk-xxx      # or ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY
```

Then trigger the workflow programmatically:

```javascript
import { WorkflowGraph } from '@zibby/core';
import { TicketTriageWorkflow } from './.zibby/workflows/ticket-triage/graph.mjs';

const agent = new TicketTriageWorkflow();
const graph = agent.buildGraph();
const result = await graph.run(agent, { input: { ticket: 'BUG-123' } });
```

Or use the CLI:

```bash
zibby start ticket-triage --port 8080
```
