Skip to main content

Your First Workflow

JJHub workflows are TSX files stored in your repository under .jjhub/workflows/. They use Smithers JSX components wrapped by the @jjhub-ai/workflow package.

Project Setup

The .jjhub/ directory at your repository root is a Bun workspace member with its own package.json and tsconfig.json. This gives you full LSP support (autocomplete, type checking, go-to-definition) when editing workflows.

.jjhub/package.json

{
  "name": "jjhub-workflows",
  "private": true,
  "type": "module",
  "dependencies": {
    "@jjhub-ai/workflow": "workspace:*"
  }
}

.jjhub/tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "smithers-orchestrator",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noUnusedLocals": false,
    "noImplicitAny": false,
    "types": ["bun-types"],
    "paths": {
      "@jjhub-ai/workflow": ["../packages/workflow/src/index.ts"]
    }
  },
  "include": ["workflows/**/*.tsx"]
}

Root package.json

Add .jjhub and packages/* to your Bun workspaces so the @jjhub-ai/workflow dependency resolves:
{
  "workspaces": ["packages/*", ".jjhub"]
}
Then run bun install to link the workspace. The @jjhub-ai/workflow package lives at packages/workflow/ and re-exports Smithers JSX components (Workflow, Task, Parallel, Sequence, Branch, Ralph) plus the on trigger builders.

Create a Workflow File

Coming Soon: We are actively working on a first-class feature to allow you to run these exact same workflows entirely on your local machine for rapid testing and debugging! Check our Roadmap for more details.
// .jjhub/workflows/ci.tsx
import { Workflow, Task, on } from "@jjhub-ai/workflow";
import { $ } from "bun";

export default (ctx) => (
  <Workflow
    name="CI"
    triggers={[on.push({ bookmarks: ["main"] }), on.landingRequest.opened()]}
  >
    <Task id="test">
      {async () => {
        await $`bun test`;
      }}
    </Task>
    <Task id="build">
      {async () => {
        await $`bun run build`;
      }}
    </Task>
  </Workflow>
);

Trigger a Run

Workflows run automatically on configured events (push, landing request opened, schedule, etc.). To manually trigger a workflow:
jjhub workflow run <workflow-id> --ref main

Manual Dispatch with Inputs

Workflows can declare inputs that users provide when triggering a run manually. This is useful for deploy workflows, release workflows, or any workflow that needs runtime parameters.

Defining Inputs

Use the on.manualDispatch trigger to declare the inputs your workflow accepts:
// .jjhub/workflows/deploy.tsx
import { Workflow, Task, on } from "@jjhub-ai/workflow";
import { $ } from "bun";

export default (ctx) => (
  <Workflow
    name="Deploy"
    triggers={[
      on.manualDispatch({
        inputs: {
          environment: {
            type: "choice",
            required: true,
            default: "staging",
            options: ["staging", "production"],
            description: "Target environment",
          },
          version: {
            type: "string",
            required: true,
            description: "Version to deploy (e.g., 1.2.3)",
          },
          dry_run: {
            type: "boolean",
            default: false,
            description: "Preview changes without applying them",
          },
          replicas: {
            type: "number",
            default: 3,
            description: "Number of replicas to deploy",
          },
        },
      }),
    ]}
  >
    <Task id="deploy">
      {async () => {
        const env = ctx.input.environment;    // "staging" | "production"
        const version = ctx.input.version;    // string
        const dryRun = ctx.input.dry_run;     // boolean
        const replicas = ctx.input.replicas;  // number

        if (dryRun) {
          console.log(`[DRY RUN] Would deploy ${version} to ${env} with ${replicas} replicas`);
          return;
        }

        await $`./scripts/deploy.ts --env ${env} --version ${version} --replicas ${replicas}`;
      }}
    </Task>
  </Workflow>
);

Input Types

TypeDescriptionExample
stringFree-form textversion: { type: "string" }
booleanTrue or falsedry_run: { type: "boolean", default: false }
numberNumeric valuereplicas: { type: "number", default: 3 }
choiceOne of a set of allowed valuesenvironment: { type: "choice", options: ["staging", "production"] }
Each input supports the following properties:
PropertyTypeDescription
typestringRequired. One of string, boolean, number, choice.
requiredbooleanWhether the input must be provided. Default: false.
defaultvariesDefault value when the input is not provided.
descriptionstringHuman-readable description shown in error messages and documentation.
optionsstring[]Required for choice type. The allowed values.

Triggering via CLI

Pass inputs using the --input (or -i) flag, one per key-value pair:
# Deploy to staging (uses default dry_run=false, replicas=3)
jjhub workflow run deploy --input environment=staging --input version=1.2.3

# Deploy to production with a dry run
jjhub workflow run deploy \
  --input environment=production \
  --input version=2.0.0 \
  --input dry_run=true

# Short form
jjhub workflow run deploy -i environment=staging -i version=1.2.3
If a required input is missing, the CLI returns an error:
Error: missing required input "version" for workflow "deploy"
If an invalid value is provided for a choice input:
Error: input "environment" must be one of: staging, production (got "dev")

Triggering via API

Send a POST request to the workflow dispatches endpoint with inputs in the request body:
curl -X POST https://api.jjhub.tech/api/repos/alice/my-project/workflows/deploy/dispatches \
  -H "Authorization: token jjhub_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "ref": "main",
    "inputs": {
      "environment": "production",
      "version": "2.0.0",
      "dry_run": true,
      "replicas": 5
    }
  }'
API endpoint:
MethodEndpointDescription
POST/api/repos/:owner/:repo/workflows/:workflow_id/dispatchesTrigger a manual workflow dispatch with inputs
Request body:
FieldTypeDescription
refstringGit ref to run on (default: main)
inputsobjectKey-value map of input names to values
The response returns the created workflow run object.

Accessing Inputs in Workflow Code

Inside any task, access inputs via ctx.input:
<Task id="notify">
  {async () => {
    const env = ctx.input.environment;
    const version = ctx.input.version;

    // Boolean inputs are actual booleans (not strings)
    if (ctx.input.dry_run) {
      console.log("Dry run mode - skipping notification");
      return;
    }

    // Number inputs are actual numbers
    console.log(`Deploying ${ctx.input.replicas} replicas`);
  }}
</Task>
For workflows triggered by other events (push, landing request, schedule), ctx.input is an empty object.

View Runs

# List runs for a workflow
jjhub run list <workflow-id>

# View a specific run
jjhub run view <run-id>

# Watch a run in real-time
jjhub run watch <run-id>

# Re-run a workflow
jjhub run rerun <run-id>

Scheduled Triggers

Workflows can run on a schedule using CRON expressions. This is useful for nightly builds, periodic reports, dependency checks, and any recurring automation.

Defining a Schedule

Use on.schedule() in your triggers array with a standard CRON expression:
// .jjhub/workflows/nightly-build.tsx
import { Workflow, Task, on } from "@jjhub-ai/workflow";
import { $ } from "bun";

export default (ctx) => (
  <Workflow
    name="Nightly Build"
    triggers={[on.schedule("0 0 * * *")]}
  >
    <Task id="build">
      {async () => {
        await $`bun run build`;
      }}
    </Task>
    <Task id="test">
      {async () => {
        await $`bun test`;
      }}
    </Task>
  </Workflow>
);

CRON Syntax

Schedules use standard five-field CRON syntax:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Common Patterns

PatternCRON ExpressionDescription
Nightly at midnight0 0 * * *Runs once per day at 00:00 UTC
Every 6 hours0 */6 * * *Runs at 00:00, 06:00, 12:00, 18:00 UTC
Hourly0 * * * *Runs at the start of every hour
Weekly on Monday 9am0 9 * * 1Runs every Monday at 09:00 UTC
Weekdays at 8am0 8 * * 1-5Runs Monday through Friday at 08:00 UTC

Timezone

All schedules run in UTC. Adjust your CRON expressions accordingly. For example, if you want a nightly build at midnight US Eastern (UTC-5), use 0 5 * * *.

Multiple Schedules

A workflow can have multiple schedule triggers, and can combine schedules with other trigger types:
// .jjhub/workflows/maintenance.tsx
import { Workflow, Task, on } from "@jjhub-ai/workflow";
import { ToolLoopAgent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { $ } from "bun";

const scanner = new ToolLoopAgent({
  model: anthropic("claude-sonnet-4-5-20250929"),
  instructions: "You are a security scanner. Report vulnerabilities found in dependencies.",
});

export default (ctx) => (
  <Workflow
    name="Maintenance"
    triggers={[
      on.schedule("0 0 * * *"),             // Nightly
      on.schedule("0 12 * * 1"),            // Monday noon
      on.push({ bookmarks: ["main"] }),     // Also on push to main
    ]}
  >
    <Task id="deps-check">
      {async () => {
        await $`bun outdated`;
      }}
    </Task>
    <Task id="security-scan" agent={scanner}>
      Scan dependencies for known vulnerabilities and report findings.
    </Task>
  </Workflow>
);

How Scheduled Workflows Run

Scheduled workflows are managed by the JJHub scheduler on the server side, not triggered by push events or user actions. When a schedule fires:
  1. The scheduler matches the CRON expression against the current UTC time
  2. A workflow run is created targeting the default bookmark (typically main)
  3. The run is queued and assigned to a runner pod from the warm pool
  4. Execution proceeds identically to any other workflow run (same logs, SSE streaming, commit statuses)
You can view scheduled runs the same way as any other run:
jjhub run list <workflow-id>

AI Agent Steps

Workflows can invoke AI agents as tasks using the agent prop:
// .jjhub/workflows/triage.tsx
import { Workflow, Task, on } from "@jjhub-ai/workflow";
import { ToolLoopAgent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const triager = new ToolLoopAgent({
  model: anthropic("claude-sonnet-4-5-20250929"),
  instructions: "You are an issue triager. Add labels and assign issues.",
});

export default (ctx) => (
  <Workflow
    name="Triage"
    triggers={[on.issue.opened()]}
  >
    <Task id="triage" agent={triager}>
      Triage this issue and add appropriate labels.
    </Task>
  </Workflow>
);
To use AI agents in workflows, configure your API key as a repository secret:
printf %s "sk-ant-..." | jjhub secret set ANTHROPIC_API_KEY --body-stdin

Schema-Driven Workflows

For AI agent workflows that pass structured data between tasks, use createSmithers to define output schemas with Zod. This gives you type-safe ctx.output() for reading previous task results and automatic SQLite-backed persistence.
// .jjhub/workflows/bugfix.tsx
import { createSmithers, Task, on } from "@jjhub-ai/workflow";
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const { Workflow, smithers } = createSmithers({
  analysis: z.object({
    summary: z.string(),
    severity: z.enum(["low", "medium", "high"]),
  }),
  fix: z.object({
    patch: z.string(),
    explanation: z.string(),
  }),
});

const analyst = new Agent({
  model: anthropic("claude-sonnet-4-5-20250929"),
  instructions: "You are a code analyst. Return structured JSON.",
});

const fixer = new Agent({
  model: anthropic("claude-sonnet-4-5-20250929"),
  instructions: "You are a senior engineer who writes minimal, correct fixes.",
});

export default smithers((ctx) => (
  <Workflow
    name="Bugfix"
    triggers={[on.manualDispatch({ inputs: { description: { type: "string", required: true } } })]}
  >
    <Task id="analyze" output="analysis" agent={analyst}>
      {`Analyze this bug: ${ctx.input.description}`}
    </Task>
    <Task id="fix" output="fix" agent={fixer}>
      {`Fix this issue: ${ctx.output("analysis", { nodeId: "analyze" }).summary}`}
    </Task>
  </Workflow>
));

How It Works

ConceptDescription
createSmithers({ ... })Defines Zod schemas for task outputs. Returns Workflow, Task, and smithers bound to those schemas.
smithers((ctx) => ...)Wraps your workflow factory with schema-aware context. Use instead of a plain export default (ctx) => ....
output="analysis"Tells the task to validate and persist its output against the analysis Zod schema.
ctx.output("analysis", { nodeId: "analyze" })Reads the typed output from a previous task. Fully type-safe — returns { summary: string, severity: "low" | "medium" | "high" }.
The simple export default (ctx) => <Workflow> pattern (shown earlier) works for compute tasks that just run shell commands. Use createSmithers when tasks need to pass structured data to each other.

Next Steps