Skip to main content

Workflows

JJHub workflows are TypeScript files that define automated tasks for your repository. There is no distinction between CI workflows and AI workflows — a workflow step can run tests, deploy code, or invoke an AI agent. They are all just code running in the same sandboxed environment. Workflows live in your repository under .jjhub/workflows/ and are triggered by events like pushes, landing request activity, schedules, or manual dispatch.

Workflow Basics

A workflow is a TypeScript file that exports a defineWorkflow() call with a name, triggers, and steps.

Minimal Example

// .jjhub/workflows/ci.ts
import { defineWorkflow, on, run } from "@jjhub/workflow";

export default defineWorkflow({
  name: "CI",
  triggers: [on.push({ bookmarks: ["main"] })],
  steps: [
    run("test", async (ctx) => {
      await ctx.exec("bun test");
    }),
  ],
});
This workflow runs bun test every time code is pushed to the main bookmark.

Workflow Structure

Every workflow has three parts:
PartDescription
nameHuman-readable name displayed in run lists and status checks
triggersEvents that cause the workflow to run
stepsSequential or parallel tasks to execute

Directory Layout

Workflows must be placed in the .jjhub/workflows/ directory at the repository root:
your-repo/
├── .jjhub/
│   └── workflows/
│       ├── ci.ts
│       ├── deploy.ts
│       ├── triage.ts
│       └── nightly.ts
├── src/
└── ...
Each .ts file in the directory is discovered automatically. The filename (without extension) becomes the workflow ID used in CLI commands like jjhub workflow run ci.

Triggers & Events

Triggers define when a workflow runs. A workflow can have multiple triggers, and will run when any of them fire.

Push

Run when code is pushed to matching bookmarks:
triggers: [
  on.push({ bookmarks: ["main"] }),
]
You can match multiple bookmarks or use glob patterns:
triggers: [
  on.push({ bookmarks: ["main", "release/*"] }),
]

Landing Request

Run when landing request activity occurs:
triggers: [
  on.landingRequest.opened(),      // LR opened or draft published
  on.landingRequest.closed(),      // LR closed without landing
  on.landingRequest.readyToLand(), // All checks pass, reviews approved
  on.landingRequest.landed(),      // LR successfully landed
]
You can combine multiple landing request events:
triggers: [
  on.landingRequest.opened(),
  on.landingRequest.readyToLand(),
]

Issue

Run when issue activity occurs:
triggers: [
  on.issue.opened(),     // New issue created
  on.issue.closed(),     // Issue closed
  on.issue.labeled(),    // Label added to issue
  on.issue.commented(),  // Comment added to issue
]

Schedule

Run on a CRON schedule (all times UTC):
triggers: [
  on.schedule("0 0 * * *"),       // Nightly at midnight UTC
  on.schedule("0 */6 * * *"),     // Every 6 hours
  on.schedule("0 9 * * 1-5"),     // Weekdays at 9am UTC
]
CRON syntax uses five fields:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
Common patterns:
PatternCRONDescription
Nightly0 0 * * *Once per day at 00:00 UTC
Every 6 hours0 */6 * * *00:00, 06:00, 12:00, 18:00 UTC
Hourly0 * * * *Start of every hour
Weekly Monday 9am0 9 * * 1Every Monday at 09:00 UTC
Weekdays 8am0 8 * * 1-5Monday—Friday at 08:00 UTC

Manual Dispatch

Run when triggered manually via CLI or API, with optional typed inputs:
triggers: [
  on.manualDispatch({
    inputs: {
      environment: {
        type: "choice",
        required: true,
        options: ["staging", "production"],
        description: "Target environment",
      },
      version: {
        type: "string",
        required: true,
        description: "Version to deploy",
      },
      dry_run: {
        type: "boolean",
        default: false,
        description: "Preview without applying",
      },
    },
  }),
]
Input types:
TypeDescription
stringFree-form text
booleanTrue or false
numberNumeric value
choiceOne of a set of allowed values (requires options)
Trigger manually from the CLI:
jjhub workflow run deploy --input environment=production --input version=1.2.3
Or via the API:
curl -X POST https://api.jjhub.tech/api/repos/owner/repo/workflows/deploy/dispatches \
  -H "Authorization: token jjhub_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "ref": "main",
    "inputs": {
      "environment": "production",
      "version": "1.2.3"
    }
  }'

Combining Triggers

A workflow can have any combination of triggers. It runs whenever any trigger fires:
triggers: [
  on.push({ bookmarks: ["main"] }),
  on.landingRequest.opened(),
  on.schedule("0 0 * * *"),
]

Webhook Trigger

Run when an external webhook event is received:
triggers: [
  on.webhook("deployment_status"),
]

Steps

Steps are the individual tasks a workflow executes. They run sequentially by default.

Run Steps

The run() function defines a step that executes shell commands:
import { defineWorkflow, on, run } from "@jjhub/workflow";

export default defineWorkflow({
  name: "CI",
  triggers: [on.push({ bookmarks: ["main"] })],
  steps: [
    run("install", async (ctx) => {
      await ctx.exec("bun install");
    }),
    run("lint", async (ctx) => {
      await ctx.exec("bun run lint");
    }),
    run("test", async (ctx) => {
      await ctx.exec("bun test");
    }),
    run("build", async (ctx) => {
      await ctx.exec("bun run build");
    }),
  ],
});
Each step has a name (used in logs and status checks) and an async function that receives a context object.

Agent Steps

The agent() function defines a step that invokes an AI agent. The agent runs in the same sandboxed environment and can read/write files, execute commands, and interact with the JJHub API:
import { defineWorkflow, on, run, agent } from "@jjhub/workflow";

export default defineWorkflow({
  name: "AI Review",
  triggers: [on.landingRequest.opened()],
  steps: [
    run("test", async (ctx) => {
      await ctx.exec("bun test");
    }),
    agent("review", async (ctx) => {
      await ctx.agent.run(
        "Review this landing request. Check for bugs, suggest improvements, and verify test coverage."
      );
    }),
  ],
});
To use agent steps, configure your AI API key as a repository secret:
jjhub secret set ANTHROPIC_API_KEY -b "sk-ant-..."

Context Object

Every step receives a ctx object with:
PropertyDescription
ctx.exec(cmd)Execute a shell command
ctx.inputsManual dispatch inputs (empty object for other triggers)
ctx.repoRepository information (owner, name)
ctx.eventThe trigger event data
ctx.agentAgent interface (available in agent() steps)

Step Failure

If any step fails (non-zero exit code or thrown error), the workflow run is marked as failed and subsequent steps are skipped. The commit status on the associated change is set to failure.

Secrets & Variables

Workflows often need credentials (API keys, deploy tokens) and configuration values. JJHub provides two mechanisms:

Secrets

Secrets are encrypted values injected as environment variables into workflow runs. They are never exposed in logs or API responses.
# Set a secret
jjhub secret set ANTHROPIC_API_KEY -b "sk-ant-..."

# Set a deploy token
jjhub secret set DEPLOY_TOKEN -b "your-deploy-token"

# List secrets (names only, values are hidden)
jjhub secret list

# Delete a secret
jjhub secret delete DEPLOY_TOKEN
Access secrets in workflow steps via environment variables:
run("deploy", async (ctx) => {
  // Secrets are available as environment variables
  await ctx.exec("./scripts/deploy.ts");
  // deploy.ts can read process.env.DEPLOY_TOKEN
});
Secrets are encrypted at rest and only injected into workflow runs. They are never visible in API responses, CLI output, or logs. If a workflow step prints a secret value to stdout, JJHub automatically redacts it in the log output.

Variables

Variables are non-secret configuration values. Unlike secrets, variable values are visible in API responses and CLI output.
# Set a variable
jjhub variable set NODE_VERSION -b "20"

# Get a variable value
jjhub variable get NODE_VERSION

# List variables
jjhub variable list

# Delete a variable
jjhub variable delete NODE_VERSION
Variables are also injected as environment variables into workflow runs.

Secrets vs Variables

AspectSecretsVariables
Values visible in API/CLINo (names only)Yes
Encrypted at restYesNo
Redacted in logsYesNo
Use caseAPI keys, tokens, passwordsBuild config, feature flags

Workflow Examples

CI Pipeline

A standard CI workflow that runs on pushes and landing requests:
// .jjhub/workflows/ci.ts
import { defineWorkflow, on, run } from "@jjhub/workflow";

export default defineWorkflow({
  name: "CI",
  triggers: [
    on.push({ bookmarks: ["main"] }),
    on.landingRequest.opened(),
  ],
  steps: [
    run("install", async (ctx) => {
      await ctx.exec("bun install");
    }),
    run("lint", async (ctx) => {
      await ctx.exec("bun run lint");
    }),
    run("typecheck", async (ctx) => {
      await ctx.exec("bun run typecheck");
    }),
    run("test", async (ctx) => {
      await ctx.exec("bun test");
    }),
    run("build", async (ctx) => {
      await ctx.exec("bun run build");
    }),
  ],
});

Deploy Workflow with Inputs

A deploy workflow triggered manually with environment selection:
// .jjhub/workflows/deploy.ts
import { defineWorkflow, on, run } from "@jjhub/workflow";

export default defineWorkflow({
  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)",
        },
      },
    }),
  ],
  steps: [
    run("deploy", async (ctx) => {
      const env = ctx.inputs.environment;
      const version = ctx.inputs.version;
      await ctx.exec(`./scripts/deploy.ts --env ${env} --version ${version}`);
    }),
  ],
});
jjhub workflow run deploy -i environment=production -i version=1.2.3

AI Issue Triage

Automatically label and assign new issues using an AI agent:
// .jjhub/workflows/triage.ts
import { defineWorkflow, on, agent } from "@jjhub/workflow";

export default defineWorkflow({
  name: "Issue Triage",
  triggers: [on.issue.opened()],
  steps: [
    agent("triage", async (ctx) => {
      await ctx.agent.run(
        "Read this issue. Add appropriate labels (bug, enhancement, documentation, question). " +
        "If it's a bug, assign to the on-call engineer. " +
        "If it's a feature request, add the 'needs-discussion' label."
      );
    }),
  ],
});

AI Code Review

Review landing requests with an AI agent after CI passes:
// .jjhub/workflows/ai-review.ts
import { defineWorkflow, on, run, agent } from "@jjhub/workflow";

export default defineWorkflow({
  name: "AI Code Review",
  triggers: [on.landingRequest.opened()],
  steps: [
    run("test", async (ctx) => {
      await ctx.exec("bun test");
    }),
    agent("review", async (ctx) => {
      await ctx.agent.run(
        "Review this landing request thoroughly. Check for:\n" +
        "- Bugs and edge cases\n" +
        "- Missing error handling\n" +
        "- Test coverage gaps\n" +
        "- Security concerns\n" +
        "Suggest specific code improvements using suggestion blocks."
      );
    }),
  ],
});

Programmable Landing Queue

Customize the landing queue behavior with a workflow:
// .jjhub/workflows/landing-queue.ts
import { defineWorkflow, on, run } from "@jjhub/workflow";

export default defineWorkflow({
  name: "Landing Queue",
  triggers: [on.landingRequest.readyToLand()],
  steps: [
    run("validate", async (ctx) => {
      await ctx.exec("bun test");
      await ctx.exec("bun run build");
    }),
  ],
});

Nightly Dependency Check

Scan for outdated or vulnerable dependencies on a schedule:
// .jjhub/workflows/deps.ts
import { defineWorkflow, on, run, agent } from "@jjhub/workflow";

export default defineWorkflow({
  name: "Dependency Check",
  triggers: [on.schedule("0 0 * * *")],
  steps: [
    run("outdated", async (ctx) => {
      await ctx.exec("bun outdated");
    }),
    agent("security", async (ctx) => {
      await ctx.agent.run(
        "Scan dependencies for known security vulnerabilities. " +
        "If critical vulnerabilities are found, create an issue with details and remediation steps."
      );
    }),
  ],
});

Combined CI + AI Workflow

A single workflow that runs tests and invokes an AI agent to explain failures:
// .jjhub/workflows/smart-ci.ts
import { defineWorkflow, on, run, agent } from "@jjhub/workflow";

export default defineWorkflow({
  name: "Smart CI",
  triggers: [
    on.push({ bookmarks: ["main"] }),
    on.landingRequest.opened(),
  ],
  steps: [
    run("test", async (ctx) => {
      try {
        await ctx.exec("bun test");
      } catch (e) {
        // Tests failed -- let the agent analyze
        throw e;
      }
    }),
    agent("analyze-failure", async (ctx) => {
      await ctx.agent.run(
        "The CI tests just failed. Read the test output, identify the root cause, " +
        "and either fix the issue by creating a new change or explain what went wrong."
      );
    }),
  ],
});

Managing Workflows

List Workflows

jjhub workflow list

Trigger a Workflow

# Trigger manually
jjhub workflow run ci --ref main

# Trigger with inputs
jjhub workflow run deploy -i environment=staging -i version=1.0.0

View Runs

# List runs for a workflow
jjhub run list ci

# View a specific run
jjhub run view 42

# Watch a run in real-time (SSE streaming)
jjhub run watch 42

# Re-run a failed workflow
jjhub run rerun 42

Commit Statuses

Workflow runs automatically create commit statuses on the associated changes. You can also manage statuses directly:
# List statuses for a change
jjhub status list <change-id>

# Set a status manually
jjhub status set <sha> --state success --context ci/test

API Endpoints

MethodEndpointDescription
GET/api/repos/:owner/:repo/workflowsList workflows
POST/api/repos/:owner/:repo/workflows/:id/dispatchesTrigger manual dispatch
GET/api/repos/:owner/:repo/runsList workflow runs
GET/api/repos/:owner/:repo/runs/:idGet run details
GET/api/repos/:owner/:repo/runs/:id/logsStream run logs (SSE)
POST/api/repos/:owner/:repo/runs/:id/cancelCancel a run
POST/api/repos/:owner/:repo/runs/:id/rerunRe-run a workflow

Next Steps