Creating Custom Workflows¶
A workflow defines what the bot does each cycle — its decision loop. Every instance has exactly one workflow, set in instance.yaml. The built-in jira-sprint workflow handles the full autonomous development loop, but you can create custom workflows for specialized use cases like monitoring, scheduled tasks, or domain-specific automation.
Built-in vs Custom Workflows¶
Built-in (core image)¶
Reference a workflow by name. It resolves from the core bot image at presets/workflows/<name>/:
Custom (instance config repo)¶
Use the ./ prefix to reference a workflow relative to your instance config repo's agent directory:
This resolves to <your-config-repo>/agent/workflows/watchduty/. Use this when your workflow is specific to your team and doesn't belong in the core bot image.
Directory Structure¶
A workflow directory contains:
workflows/my-workflow/
├── CLAUDE.md # Required — the bot's instructions for this workflow
├── manifest.yaml # Recommended — metadata, requirements, preflight declaration
├── preflight/ # Optional — pre-session data-gathering scripts
│ ├── 01-check-service.py
│ └── 02-check-capacity.py
└── skills/ # Optional — workflow-specific skill scripts
└── my-skill/
├── SKILL.md
└── my_skill.py
Required: CLAUDE.md¶
This is the core of your workflow. At startup, the runner assembles the final CLAUDE.md by combining:
- Core instructions (
presets/core/CLAUDE.md) — security rules, tool usage, general behavior - Workflow instructions (your workflow's
CLAUDE.md) — what to do each cycle - Instance instructions (your config repo's
agent/CLAUDE.md) — instance-specific overrides (strategy-dependent)
Your workflow CLAUDE.md should be written as imperative instructions telling the bot what to do. Think of it as a runbook.
Recommended: manifest.yaml¶
Declares metadata and requirements. Validated at startup — missing required MCP servers or env vars cause a fatal error, catching deployment misconfigurations early.
Optional: preflight/¶
Pre-session scripts that run before each Claude session. See Writing Custom Preflight Scripts for the full guide.
Optional: skills/¶
Python scripts that the bot can invoke during its session (e.g. triage logic, data fetching). These are copied to .claude/skills/ at startup.
manifest.yaml Schema¶
name: my-workflow
type: workflow
description: Brief description of what this workflow does
# Preflight scripts (documentation — discovery is filesystem-based)
preflight:
- 01-check-service.py
- 02-check-capacity.py
# Core skills to include alongside this workflow's own skills
shared_skills:
- push-and-pr # Git push + PR creation
- post-pr # Post-PR Jira comment + Slack notification
- auto-fork # Fork creation for new repos
# What this workflow provides
provides:
claude_md: CLAUDE.md
skills:
- my-skill # Skill directories under this workflow's skills/
# What this workflow requires to function
requires:
mcp_servers:
- bot-memory # Must be configured in MCP settings
- mcp-atlassian
env_vars:
- BOT_LABEL # Fatal if not set
- BOT_INSTANCE_ID
optional_env_vars:
- SLACK_WEBHOOK_URL # Warning if not set, but not fatal
Field Reference¶
| Field | Required | Description |
|---|---|---|
name |
Yes | Workflow identifier |
type |
Yes | Must be workflow |
description |
Yes | One-line summary |
preflight |
No | List of preflight script filenames (documentation only) |
shared_skills |
No | Core skill names to include (from presets/shared/skills/) |
provides.claude_md |
No | CLAUDE.md filename (always CLAUDE.md) |
provides.skills |
No | Skill directory names provided by this workflow |
requires.mcp_servers |
No | MCP servers that must be configured. Fatal if missing. |
requires.env_vars |
No | Environment variables that must be set. Fatal if missing. |
requires.optional_env_vars |
No | Env vars that should be set. Warning if missing. |
instance.yaml Configuration¶
One instance = one workflow. Each instance directory maps to its own OpenShift deployment (own pod, own schedule, own task queue). If you need a second workflow for the same repos, create a second instance directory:
instance/
rbac-config/ # Jira-driven workflow
agent/
instance.yaml # workflow: jira-kanban
project-repos.json
rbac-config-konflux/ # Separate workflow for the same repos
agent/
instance.yaml # workflow: ./workflows/konflux-pr-squash
project-repos.json # Can reference the same repos
workflows/
konflux-pr-squash/
...
Both instances share the memory server and can see each other's tasks, but run independently on their own schedules.
Your instance config repo's instance.yaml controls which workflow runs and how:
# instance/<config>/agent/instance.yaml
workflow: ./workflows/my-workflow # ./ = relative to this agent dir
source: scheduled # or "jira"
envs:
- slack
- node
claude_md:
strategy: append # ignore | append | replace
Fields¶
| Field | Default | Description |
|---|---|---|
workflow |
jira-sprint |
Workflow name (built-in) or ./path (custom) |
source |
jira |
Ticket source. jira = Jira sprint polling. scheduled = time-based. |
envs |
null (all) |
Env presets to activate. null = all available, [] = none. |
claude_md.strategy |
ignore |
How instance CLAUDE.md combines with workflow CLAUDE.md |
CLAUDE.md Assembly Strategies¶
The claude_md.strategy field controls how your instance's agent/CLAUDE.md combines with the workflow's CLAUDE.md:
ignore (default)¶
Instance CLAUDE.md is ignored. Use when the workflow's instructions are complete and the instance doesn't need to add anything.
When to use: Most workflows. The workflow CLAUDE.md has everything the bot needs.
append¶
Instance CLAUDE.md is appended after the workflow instructions. Use when you want to add instance-specific context on top of the workflow.
When to use: Your team has extra context (team conventions, specific repos, escalation contacts) that supplements the workflow's decision loop.
<!-- agent/CLAUDE.md (instance) -->
## Team-Specific Context
- Our Jenkins is at jenkins.example.com
- Escalate P0 issues to #team-oncall in Slack
- Prod jobs are prefixed with "prod-"
replace¶
The workflow's CLAUDE.md is skipped entirely. Instance CLAUDE.md replaces it. Use when you want complete control over the bot's instructions while still keeping core security rules.
When to use: Rarely. Only when the workflow is just a container for preflight/skills and the instructions are fully custom per instance.
Path Resolution¶
The resolve_workflow_dir() function in bot/config.py handles path resolution:
- Plain name (e.g.
jira-sprint) →<bot-repo>/presets/workflows/jira-sprint/ ./prefix (e.g../workflows/watchduty) →<config-repo>/agent/workflows/watchduty/
This resolution is used consistently across:
- Startup validation (validate_instance_config)
- Manifest loading and validation (load_manifest, validate_manifest)
- CLAUDE.md assembly (assemble_claude_md)
- Preflight script discovery (discover_preflight_scripts)
What Workflows Don't Handle¶
Workflows define the decision loop. Everything else comes from instance config merging:
| Handled by workflow | Handled by instance config merge |
|---|---|
| CLAUDE.md instructions | Personas |
| Preflight scripts | Skills (instance-level) |
| Workflow-specific skills | MCP server configs |
Settings (.claude/settings.json) |
|
Hooks (.claude/hooks/) |
|
project-repos.json |
The merge system (bot/merge.py → apply_merged_config()) combines persona files, MCP configs, skills, settings, and hooks from both the core image and your instance config. Workflows are independent of this — they provide their own CLAUDE.md and preflight scripts.
Complete Example: Review-Only Workflow¶
Let's build a workflow that only reviews PRs — no new Jira work, no ticket claiming.
1. Create the directory¶
instance/my-config/agent/
├── instance.yaml
├── workflows/
│ └── review-only/
│ ├── CLAUDE.md
│ ├── manifest.yaml
│ └── preflight/
│ └── 01-check-prs.py
└── project-repos.json
2. Write the CLAUDE.md¶
# Review-Only Workflow
Each cycle: check for PRs needing review, review them, post feedback.
## Cycle Loop
1. Read the preflight data — it contains PR statuses and review requests.
2. If no PRs need attention, stop.
3. For each PR needing review (one per cycle):
a. Clone the repo if needed
b. Read the diff
c. Post a code review with actionable feedback
d. Update task tracking
## Rules
- ONE PR per cycle
- Only review, never push code
- Be constructive — suggest improvements, don't just list problems
3. Write the manifest.yaml¶
name: review-only
type: workflow
description: Review PRs without implementing new work
preflight:
- 01-check-prs.py
shared_skills:
- push-and-pr # For reading PR templates/conventions
provides:
claude_md: CLAUDE.md
requires:
mcp_servers:
- bot-memory
env_vars:
- BOT_INSTANCE_ID
optional_env_vars:
- SLACK_WEBHOOK_URL
4. Write the preflight script¶
#!/usr/bin/env python3
"""Pre-flight: check for PRs needing review."""
import json
import sys
from common import get_tasks, get_task_prs, output_result
def main():
tasks = get_tasks()
review_needed = []
for task in tasks:
if task.get("status") not in ("pr_open", "pr_changes"):
continue
prs = get_task_prs(task)
if prs:
review_needed.append(
{
"key": task.get("external_key", "?"),
"repo": task.get("repo", "?"),
"prs": prs,
}
)
if not review_needed:
output_result("skip", f"{len(tasks)} tasks, none with PRs needing review.")
return
content = f"{len(review_needed)} PRs need review:\n"
content += json.dumps(review_needed, indent=2)
output_result("start", content)
if __name__ == "__main__":
main()
5. Configure instance.yaml¶
workflow: ./workflows/review-only
source: jira
envs:
- slack
claude_md:
strategy: ignore # Workflow CLAUDE.md has everything needed
Real-World Pattern: Scheduled Monitoring¶
For workflows that run on a schedule (not driven by Jira), use source: scheduled and a preflight script that checks an external service:
# instance.yaml
workflow: ./workflows/watchduty
source: scheduled
envs:
- slack
claude_md:
strategy: append
The key differences from a Jira-driven workflow:
- source: scheduled — The bot runs on a timer rather than polling Jira for new tickets
- Preflight decides everything — No Jira triage phase. The preflight script checks the external service and determines whether action is needed.
- strategy: append — Instance CLAUDE.md adds team-specific context (service URLs, escalation paths) on top of the workflow's general instructions.
The preflight script for this pattern typically:
1. Fetches data from the external service (Jenkins, Grafana, custom API)
2. Classifies items deterministically (failing vs healthy, priority ordering)
3. Outputs start with only the actionable items, or skip if everything is healthy
See Writing Custom Preflight Scripts for the implementation details.
Task Tracking¶
Custom workflows use the same memory server task system as built-in workflows. The agent creates and updates tasks via MCP tools (task_add, task_update, task_get, task_remove) — never via raw HTTP calls to the memory server API.
external_key conventions¶
Every task has an external_key + source_type pair. The DB enforces UNIQUE(external_key, source_type), so this combo is the task's identity.
For Jira-driven workflows, the external_key is the Jira ticket key (RHCLOUD-12345) and source_type defaults to "jira".
For non-Jira workflows, you define your own key format. Use a deterministic, human-readable composite key:
Examples:
- konflux-pr-squash:project-kessel/insights-rbac — consolidation task scoped to a repo
- watchduty:jenkins-prod-pipeline — monitoring task scoped to a service
- review-only:RedHatInsights/insights-chrome — review task scoped to a repo
If a workflow creates multiple tasks per scope (e.g. one consolidated PR per ecosystem), extend the key:
- konflux-pr-squash:project-kessel/insights-rbac:go
- konflux-pr-squash:project-kessel/insights-rbac:python
source_type¶
The task_add MCP tool defaults source_type to "jira". Non-Jira workflows must pass it explicitly:
task_add(
external_key="konflux-pr-squash:project-kessel/insights-rbac",
source_type="github", # NOT the default "jira"
repo="insights-rbac",
branch="bot/consolidate-go-deps",
status="pr_open",
title="Consolidate 5 Go dependency updates",
metadata={
"prs": [{"repo": "project-kessel/insights-rbac", "number": 42, "host": "github"}],
"original_prs": [101, 102, 103],
"ecosystem": "go"
}
)
Getting source_type wrong means task_get lookups fail (it queries by external_key + source_type), and preflight scripts that check for in-progress tasks won't find them.
Why MCP, not HTTP¶
Your workflow CLAUDE.md should tell the agent to use task_add, not construct HTTP requests. The MCP tool:
- Enforces the capacity cap (max 10 active tasks) — raw HTTP bypasses this
- Publishes events to the SSE bus (dashboard updates, Slack notifications)
- Builds artifact links automatically (Jira URLs, PR links)
- Validates input and returns structured errors
Preflight integration¶
Preflight scripts check the task system to avoid duplicate work. Your workflow's preflight should:
- Call
get_tasks()to fetch active tasks - Filter for tasks matching your workflow's
external_keyprefix - Skip if a matching task is already in progress
from common import get_tasks, get_capacity, output_result
TASK_KEY_PREFIX = "my-workflow:"
tasks = get_tasks()
active = [t for t in tasks if t.get("status") in ("in_progress", "pr_open", "pr_changes")]
# Already working on this?
my_tasks = [t for t in active if t.get("external_key", "").startswith(TASK_KEY_PREFIX)]
if my_tasks:
output_result("skip", f"Already in progress: {my_tasks[0]['external_key']}")
return
# Respect capacity
active_n, max_n = get_capacity()
if active_n >= max_n:
output_result("skip", f"At capacity ({active_n}/{max_n})")
return
The built-in gh_pr_status.py preflight monitors all pr_open tasks automatically — no per-workflow polling needed. Create a task with status="pr_open" and PR metadata, and CI monitoring happens for free.
Checklist: Shipping a Custom Workflow¶
-
Create the workflow directory in your config repo under
agent/workflows/<name>/ -
Write
CLAUDE.md— imperative instructions for the bot's decision loop. Be specific about: - What to check each cycle
- When to take action vs skip
- How to report results
-
What "done" looks like
-
Write
manifest.yaml— declare requirements so deployment failures are caught at startup, not mid-session -
Add preflight scripts (if needed) — pre-session data gathering to avoid wasting tokens on idle cycles
-
Update
instance.yaml— setworkflow: ./workflows/<name>and choose the rightclaude_md.strategy -
Test locally:
-
Deploy — set
BOT_CONFIG_PATHin your deployment template to point to your instance config
Related Docs¶
- Workflow presets — Built-in workflow reference (jira-sprint)
- Writing custom preflight scripts — Detailed guide to preflight scripts
- Env presets — Available env presets for tools and runtimes
- Presets overview — How all preset types work together
- Presets design doc — Architecture decisions and rationale