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:
| Part | Description |
|---|
name | Human-readable name displayed in run lists and status checks |
triggers | Events that cause the workflow to run |
steps | Sequential 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:
| Pattern | CRON | Description |
|---|
| Nightly | 0 0 * * * | Once per day at 00:00 UTC |
| Every 6 hours | 0 */6 * * * | 00:00, 06:00, 12:00, 18:00 UTC |
| Hourly | 0 * * * * | Start of every hour |
| Weekly Monday 9am | 0 9 * * 1 | Every Monday at 09:00 UTC |
| Weekdays 8am | 0 8 * * 1-5 | Monday—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:
| Type | Description |
|---|
string | Free-form text |
boolean | True or false |
number | Numeric value |
choice | One 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:
| Property | Description |
|---|
ctx.exec(cmd) | Execute a shell command |
ctx.inputs | Manual dispatch inputs (empty object for other triggers) |
ctx.repo | Repository information (owner, name) |
ctx.event | The trigger event data |
ctx.agent | Agent 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
| Aspect | Secrets | Variables |
|---|
| Values visible in API/CLI | No (names only) | Yes |
| Encrypted at rest | Yes | No |
| Redacted in logs | Yes | No |
| Use case | API keys, tokens, passwords | Build 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");
}),
],
});
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
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
| Method | Endpoint | Description |
|---|
GET | /api/repos/:owner/:repo/workflows | List workflows |
POST | /api/repos/:owner/:repo/workflows/:id/dispatches | Trigger manual dispatch |
GET | /api/repos/:owner/:repo/runs | List workflow runs |
GET | /api/repos/:owner/:repo/runs/:id | Get run details |
GET | /api/repos/:owner/:repo/runs/:id/logs | Stream run logs (SSE) |
POST | /api/repos/:owner/:repo/runs/:id/cancel | Cancel a run |
POST | /api/repos/:owner/:repo/runs/:id/rerun | Re-run a workflow |
Next Steps