CLI Reference
Boilermaker ships a boilermaker command for inspecting and managing TaskGraph state directly from Azure Blob Storage and Azure Service Bus. It is useful for diagnosing stalled graphs in production without touching application code.
Installation
The CLI is included when you install the package:
Global options
--storage-url and --container are required by all commands and go before the subcommand name. They can also be provided via environment variables:
| Option | Env var | Description |
|---|---|---|
--storage-url URL |
BOILERMAKER_STORAGE_URL |
Azure Blob Storage account URL |
--container NAME |
BOILERMAKER_CONTAINER |
Blob container name |
-v / --verbose |
— | Enable debug logging |
--no-color |
NO_COLOR |
Disable colored output |
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
<subcommand> [subcommand options]
Setting BOILERMAKER_STORAGE_URL and BOILERMAKER_CONTAINER in your environment lets you omit those flags entirely.
Authentication
The CLI uses DefaultAzureCredential for blob storage access. Make sure your environment is authenticated (e.g. via az login, a managed identity, or environment variables).
Commands that publish to Service Bus (recover, invoke) also use DefaultAzureCredential for Service Bus access.
Commands
inspect
Print a rich status panel and task table for a TaskGraph. Pass --task to drill into a single task.
boilermaker --storage-url <url> --container <name> inspect \
--graph <graph_id> \
[--task <task_id>] \
[--json]
Options
| Option | Required | Description |
|---|---|---|
--graph GRAPH_ID |
Yes | The ID of the graph to inspect |
--task TASK_ID |
No | Inspect a single task (requires --graph) |
--json |
No | Output machine-readable JSON instead of formatted output |
Example — inspect a graph
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
inspect --graph "$GRAPH_ID"
Output (color-coded in a real terminal):
╭─────────────────────────────────────────────╮
│ Graph: 019d8c0c-bd9b-7c23-be84-4d0799d7ecd4 │
│ Status: In Progress │
│ Complete: 2/3 │
│ Failures: 0 Stalled: 1 │
╰─────────────────────────────────────────────╯
Task ID (short) Function Status Type
────────────── ────────────────── ────────── ──────────
be84-4c416d85 fetch_data ✓ Success child
be84-4c5388b8 process_report ⚠ Retry child STALLED
be84-4c6a1b2c send_notification · Pending child
A task is considered stalled if its status is Scheduled, Started, or Retry — states that indicate the task was dispatched but no worker has written a terminal result.
Example — inspect a single task
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
inspect --graph "$GRAPH_ID" --task "$TASK_ID"
Output:
╭─── Task Detail ────────────────────────────────────────────╮
│ Task ID: 019d8c0c-bd9b-7c23-be84-4c5388b8de6c │
│ Function: process_report │
│ Status: ⚠ Retry STALLED │
│ Type: child │
│ Graph: 019d8c0c-bd9b-7c23-be84-4d0799d7ecd4 │
│ │
│ Dependencies: fetch_data │
│ Dependents: send_notification │
╰────────────────────────────────────────────────────────────╯
Example — JSON output
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
inspect --graph "$GRAPH_ID" --json
{
"graph_id": "019d8c0c-bd9b-7c23-be84-4d0799d7ecd4",
"is_complete": false,
"has_failures": false,
"stalled_count": 1,
"tasks": [
{
"task_id": "019d8c0c-bd9b-7c23-be84-4c416d85779d",
"function_name": "fetch_data",
"status": "Success",
"type": "child",
"is_stalled": false,
"depends_on": []
}
],
"fail_tasks": []
}
Exit codes
| Code | Meaning |
|---|---|
0 |
Graph is healthy — no stalled tasks |
1 |
One or more stalled tasks found |
2 |
Error (graph not found, missing arguments, etc.) |
inspect is safe to use in scripts and health checks:
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
inspect --graph "$GRAPH_ID"
if [ $? -eq 1 ]; then
echo "Graph has stalled tasks — consider running recover"
fi
recover
Re-publish all stalled tasks for a graph to Service Bus. Each recovery message uses a unique message ID (<task_id>:recovery:<timestamp>) so it bypasses Service Bus duplicate detection.
boilermaker --storage-url <url> --container <name> recover \
--graph <graph_id> \
--sb-namespace-url <url> \
--sb-queue-name <name>
Options
| Option | Required | Description |
|---|---|---|
--graph GRAPH_ID |
Yes | The ID of the graph to recover |
--sb-namespace-url URL |
Yes | Service Bus namespace URL (e.g. https://myns.servicebus.windows.net) |
--sb-queue-name NAME |
Yes | Service Bus queue name |
Example
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
recover \
--graph "$GRAPH_ID" \
--sb-namespace-url "$SERVICE_BUS_NAMESPACE_URL" \
--sb-queue-name "$QUEUE_NAME"
Output:
✓ Recovered: be84-4c5388b8 (process_report) — msg_id: 019d...:recovery:1713095548
✗ Failed: be84-4c6a1b2c (send_notification) — <error message>
Exit codes
| Code | Meaning |
|---|---|
0 |
All stalled tasks recovered successfully |
1 |
One or more tasks failed to recover |
2 |
Error (graph not found, missing arguments, etc.) |
Recovery re-executes tasks
Recovery publishes the task again to the Service Bus queue. If the original task execution is still running (e.g. a very slow worker), both copies may run concurrently. Ensure your task handlers are idempotent before using recover.
purge
Delete old task-result blobs from Azure Blob Storage.
boilermaker --storage-url <url> --container <name> purge \
--older-than <days> \
[--dry-run] \
[--force] \
[--all-graphs]
How "old" is decided
purge groups blobs by graph and keeps or deletes each group as a unit. A group is eligible when its age is more than --older-than days — where age comes from the created_date tag (default discovery) or, with --all-graphs, from the millisecond timestamp embedded in the UUID7 graph_id.
By default, discovery uses the Azure blob tag index (created_date < cutoff): fast, server-side, no full container scan. It only sees graphs that carry a created_date tag — graphs written before tagging existed (or otherwise untagged) are invisible to it. To sweep those, use --all-graphs, which instead lists every graph-id prefix and dates each graph by the millisecond timestamp embedded in its UUID7 graph_id. That path is slower (it scans prefixes) but finds untagged/legacy graphs. purge uses one path or the other — never both.
Cleaning up legacy (pre-tag) graphs
If your container has graphs created before created_date tagging was in use, the default run will not find them. Run purge --older-than <days> --all-graphs (start with --dry-run) to reach them.
What gets deleted vs. protected
| Data | Default behavior | With --force |
|---|---|---|
| Graph, all tasks finished | Deleted when old | Deleted when old |
Graph with in-progress tasks (scheduled, started, retry) |
Skipped quietly (counted in the summary, exit code 1) |
Deleted when old |
Standalone task-result (a task run outside a graph — no graph.json), finished |
Deleted when old | Deleted when old |
| Standalone task-result still in progress | Skipped quietly (counted in the summary, exit code 1) |
Deleted when old |
Corrupted graph — missing graph.json but still holds task blobs, so its status can't be verified |
Skipped and logged as a warning | Deleted when old |
In-progress work is a normal, expected state, so it is skipped without noise — you see it in the run summary and the exit code, not as a per-item message. Diagnostics are emitted through the standard logging module (logger boilermaker.cli), so you can route or suppress them like any other logs; a warning is logged only for genuinely corrupted data (a directory with task-result blobs but no graph.json, or a graph.json that fails to load), so a warning always means "something here needs a look."
Within each eligible group, task-result blobs are deleted first and graph.json last; if any task-result deletion fails, that group's graph.json is left in place so the group is retried on the next run rather than orphaned.
Options
| Option | Required | Description |
|---|---|---|
--older-than DAYS |
Yes | Delete graphs older than DAYS days (1–30 inclusive). By default, age comes from the created_date tag; with --all-graphs it comes from the UUID7 graph_id timestamp. |
--dry-run |
No | Print what would be deleted without deleting any blobs |
--force |
No | Also delete old graphs/tasks that are still in progress, and old corrupted graphs that are missing graph.json. Use with care — this removes work whose status could not be verified. |
--all-graphs |
No | Slower, thorough discovery: skip the tag index and list every graph-id prefix, dating each graph by its UUID7 timestamp. Use this to find untagged/pre-tag graphs the default (tag-based) discovery cannot see. |
Example — dry run first, then execute
# Step 1: preview what will be deleted
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
purge --older-than 7 --dry-run
# Step 2: execute after confirming the plan
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
purge --older-than 7
Dry-run output
Purge Plan
Blobs last modified before 2026-04-07 (older than 7 days)
Graph Blobs
────────────────────────────────────────── ─────
019d8c0c-bd9b-7c23-be84-4d0799d7ecd4 12
019d8c0c-bd9b-7c23-be84-4d0799d7ecd5 3
Total: 2 graphs, 15 blobs
[DRY RUN] No blobs were deleted.
Only corrupted data is logged (as a WARNING on the boilermaker.cli logger) and excluded from the plan; in-progress work is skipped silently — see the summary and exit code instead:
WARNING boilermaker.cli Graph 019d8c0c-... appears corrupted — it has task-result blobs but no graph.json, so its status cannot be verified. Skipping (use --force to delete).
Pass --force to delete corrupted and in-progress groups anyway.
Post-deletion output
Exit codes
| Code | Meaning |
|---|---|
0 |
Success — no errors, nothing skipped for in-progress tasks (or dry-run completed) |
1 |
One or more graphs/tasks skipped because they are still in progress |
2 |
Unrecoverable error (auth failure, container not found, all deletions failed) |
Corrupted data does not change the exit code
A logged warning for a corrupted group is there for a human to investigate; it does not on its own make the run exit non-zero. Only in-progress skips set exit code 1.
Deletion is irreversible
Deleted blobs cannot be recovered unless Azure soft-delete is enabled on the storage account. Always run with --dry-run first to confirm the scope. Concurrent purge invocations against the same container are safe — any blob already deleted by a concurrent process returns a 404, which is treated as a no-op.
invoke
Publish a single task to Service Bus. Useful for manually triggering a specific task without re-running the whole graph.
boilermaker --storage-url <url> --container <name> invoke <task_id> \
--graph <graph_id> \
--sb-namespace-url <url> \
--sb-queue-name <name> \
[--force]
Options
| Option | Required | Description |
|---|---|---|
TASK_ID |
Yes | Positional — the task ID to invoke |
--graph GRAPH_ID |
Yes | The graph the task belongs to |
--sb-namespace-url URL |
Yes | Service Bus namespace URL |
--sb-queue-name NAME |
Yes | Service Bus queue name |
--force |
No | Allow re-invocation of tasks already in a terminal state |
Example
boilermaker \
--storage-url "$AZURE_STORAGE_URL" \
--container "$CONTAINER_NAME" \
invoke "$TASK_ID" \
--graph "$GRAPH_ID" \
--sb-namespace-url "$SERVICE_BUS_NAMESPACE_URL" \
--sb-queue-name "$QUEUE_NAME"
Output:
If the task is already in a terminal state (Success, Failure, RetriesExhausted, etc.) without --force:
Exit codes
| Code | Meaning |
|---|---|
0 |
Task published successfully |
2 |
Error (graph/task not found, terminal state without --force, publish failed) |
Invoke re-executes a task
invoke publishes the task to the queue without resetting its result blob. If the worker picks it up, it will overwrite the existing result. Use --force intentionally and ensure your task handler is idempotent.