OpenCTI Playbook Manager: How to Create Automated Workflows
The Playbook Manager is OpenCTI's core background service that automates workflows by orchestrating playbook components in response to live STIX events or scheduled cron triggers.
The OpenCTI Playbook Manager powers the platform's automation engine, enabling security teams to build reactive data pipelines without custom code. This system continuously monitors platform events and schedules, executing defined sequences of components that enrich, transform, and share threat intelligence. Understanding how to leverage the playbook manager is essential for creating automated workflows that scale with your CTI operations.
What is the Playbook Manager?
The Playbook Manager is the daemon process responsible for evaluating and executing playbooks—JSON-defined workflows composed of interconnected components. According to the OpenCTI source code in opencti-platform/opencti-graphql/src/manager/playbookManager/playbookManager.ts, the manager operates as a distributed service that acquires locks to ensure singleton execution across clustered deployments.
Core Architecture and Components
Located in playbookManager.ts, the manager initializes two asynchronous schedulers within initPlaybookManager:
streamScheduler: Listens to platform SSE events viaplaybookStreamHandlercronScheduler: Triggers time-based workflows viahandlePlaybookCrons
The system uses Redis distributed locks (playbook_manager:lock_key and playbook_manager:lock_cron_key) to guarantee that only one active instance processes events or cron jobs at a time.
Individual components are executed by playbookExecutor in opencti-platform/opencti-graphql/src/manager/playbookManager/playbookExecutor.ts. This module runs component logic, persists execution metadata to Redis via redisPlaybookUpdate, and recursively follows port links to subsequent steps.
Built-in components are defined in opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts, including:
PLAYBOOK_INTERNAL_DATA_STREAMfor event-driven triggersPLAYBOOK_INTERNAL_DATA_CRONfor scheduled executionPLAYBOOK_CONNECTOR_COMPONENTfor enrichment integrationsPLAYBOOK_CONTAINER_WRAPPER_COMPONENTfor report generation
Execution Flow for Live Events
When STIX objects are created or modified, the Playbook Manager processes them through the following pipeline (lines 58-127 of playbookManager.ts):
playbookStreamHandlerreceives the SSE event and iterates over cached playbook definitions fromgetEntitiesListFromCache- For each playbook starting with
PLAYBOOK_INTERNAL_DATA_STREAM, it validates:- Event source is not the same playbook (
origin?.playbook_id !== playbook.internal_id) - Event type matches
configurationfilters viaisValidEventType - Payload satisfies STIX filters via
isStixMatchFilterGroup
- Event source is not the same playbook (
- Upon validation, a STIX bundle is constructed and passed to
playbookExecutor - The executor runs the component's
executorfunction, logs the step, and followslinksto the next node
Execution Flow for Cron Schedules
Time-based workflows follow a different path (lines 261-361 of playbookManager.ts):
- Every minute, the cron handler acquires the lock and invokes
handlePlaybookCrons - For playbooks with
PLAYBOOK_INTERNAL_DATA_CRONstart nodes,shouldTriggerNowevaluates if the current time matches the configuredperiodandtriggerTime - When triggered, the system converts filters to query options via
convertFiltersToQueryOptionsand loads matching entities usingstixLoadByFiltersorelPaginate - The resulting STIX bundle feeds into the same
playbookExecutorpipeline as live events
How to Create Automated Workflows in OpenCTI
Playbooks are stored as JSON definitions in the playbook_definition column and exposed through GraphQL mutations defined in opencti-platform/opencti-graphql/src/modules/playbook/playbook.graphql.
Defining Playbook Structure
A playbook definition consists of nodes (components) and links (connections). The TypeScript interfaces in opencti-platform/opencti-graphql/src/modules/playbook/playbook-types.ts define:
ComponentDefinition: Containsid,component_id,configuration, andpositionNodeDefinition: Defines port connections between nodes
Creating a Stream-Based Workflow
To create a workflow that logs all new STIX objects, use the playbookAdd mutation with a definition containing PLAYBOOK_INTERNAL_DATA_STREAM and PLAYBOOK_LOGGER_COMPONENT:
mutation AddLogPlaybook {
playbookAdd(
input: {
name: "Log on Stream"
description: "Every new STIX object is logged"
playbook_start: "node-1"
playbook_mode: "stream"
playbook_definition: """
{"nodes":[{"id":"node-1","name":"Listen on stream","component_id":"PLAYBOOK_INTERNAL_DATA_STREAM","configuration":"{\"create\":true,\"update\":false,\"delete\":false,\"filters\":\"{}\"}","position":{"x":100,"y":100}},{"id":"node-2","name":"Log bundle","component_id":"PLAYBOOK_LOGGER_COMPONENT","configuration":"{\"level\":\"info\"}","position":{"x":300,"y":100}}],"links":[{"id":"link-1","from":{"id":"node-1","port":"out"},"to":{"id":"node-2"}}]}
"""
}
) {
id
name
}
}
The configuration field contains JSON-serialized settings for each component. For stream components, this includes boolean flags for create, update, and delete events.
Creating a Cron-Based Workflow
For scheduled reporting, use PLAYBOOK_INTERNAL_DATA_CRON with PLAYBOOK_CONTAINER_WRAPPER_COMPONENT:
mutation AddCronReport {
playbookAdd(
input: {
name: "Weekly Report Builder"
description: "Every Monday at 09:00 generate a Report container"
playbook_start: "node-1"
playbook_mode: "cron"
playbook_definition: """
{
"nodes":[
{
"id":"node-1",
"name":"Weekly trigger",
"component_id":"PLAYBOOK_INTERNAL_DATA_CRON",
"configuration":"{\"period\":\"week\",\"triggerTime\":\"1-09:00:00.000Z\"}",
"position":{"x":100,"y":100}
},
{
"id":"node-2",
"name":"Wrap in Report",
"component_id":"PLAYBOOK_CONTAINER_WRAPPER_COMPONENT",
"configuration":"{\"container_type\":\"Report\"}",
"position":{"x":300,"y":100}
}
],
"links":[
{"id":"link-1","from":{"id":"node-1","port":"out"},"to":{"id":"node-2"}}
]
}
"""
}
) {
id
name
}
}
The triggerTime format follows day-hour:minute:second.millisecondZ where day 1 represents Monday.
Triggering Manual Execution
To run a playbook against existing data, use the playbookManualExecution mutation:
mutation RunPlaybook {
playbookManualExecution(
playbook_id: "playbook-id-xyz"
entity_id: "abcd1234-..."
)
}
This invokes executePlaybookOnEntity in playbookManager.ts (lines 34-48), which loads the entity, constructs a temporary STIX bundle, and initiates the executor chain.
Extending the Playbook System
Building Custom Components
Developers can extend opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts by implementing the PlaybookComponent<T> interface:
import type { PlaybookComponent } from '../../modules/playbook/playbook-types';
import { type JSONSchemaType } from 'ajv';
interface MyCompConfig {
message: string;
}
const MyCompSchema: JSONSchemaType<MyCompConfig> = {
type: 'object',
properties: { message: { type: 'string' } },
required: ['message'],
};
export const PLAYBOOK_MY_MESSAGE_COMPONENT: PlaybookComponent<MyCompConfig> = {
id: 'PLAYBOOK_MY_MESSAGE_COMPONENT',
name: 'Emit a custom message',
description: 'Adds a custom text field to the bundle',
icon: 'info',
is_entry_point: false,
is_internal: true,
ports: [{ id: 'out', type: 'out' }],
configuration_schema: MyCompSchema,
schema: async () => MyCompSchema,
executor: async ({ bundle, playbookNode }) => {
const customObj = {
id: `custom--${Date.now()}`,
type: 'x-opencti-custom',
spec_version: '2.1',
extensions: { 'extension-definition--xxxx': { message: playbookNode.configuration.message } },
};
bundle.objects.push(customObj);
return { output_port: 'out', bundle, forceBundleTracking: true };
},
};
Register the component in the PLAYBOOK_COMPONENTS map at the bottom of playbook-components.ts to make it available in the UI palette.
Programmatic Execution via API
For external automation, trigger playbooks using the OpenCTI client:
import OpenCTIAPIClient from 'opencti-graphql-client';
const client = new OpenCTIAPIClient('http://localhost:4000', { token: 'YOUR_TOKEN' });
async function runPlaybookOnEntity(playbookId: string, entityId: string) {
const result = await client.graphql(`
mutation Run($playbookId: ID!, $entityId: StixRef!) {
playbookManualExecution(playbook_id: $playbookId, entity_id: $entityId)
}
`, { playbookId, entityId });
console.log('Playbook dispatched', result);
}
Summary
- The Playbook Manager in
playbookManager.tsis a singleton daemon that orchestrates automated workflows through stream and cron schedulers - Playbook definitions are JSON structures containing nodes and links, stored via the GraphQL
playbookAddmutation - Live event processing uses
playbookStreamHandlerto filter SSE events against playbook configurations and execute matching chains viaplaybookExecutor - Cron workflows trigger periodically through
handlePlaybookCrons, building STIX bundles from filtered entity queries - Custom components can be added by implementing the
PlaybookComponentinterface inplaybook-components.tsand registering them in the component map - Manual execution allows ad-hoc playbook runs against specific entities using the
playbookManualExecutionGraphQL mutation
Frequently Asked Questions
What is the difference between stream and cron playbooks in OpenCTI?
Stream playbooks react to real-time platform events via the PLAYBOOK_INTERNAL_DATA_STREAM component, processing STIX objects as they are created or modified. Cron playbooks use the PLAYBOOK_INTERNAL_DATA_CRON component to execute on fixed schedules (minute, hour, day, week), querying the database for matching entities at trigger time.
How does the Playbook Manager ensure only one instance runs in a cluster?
The manager acquires distributed Redis locks using playbook_manager:lock_key for stream processing and playbook_manager:lock_cron_key for cron handling. These locks ensure that even in horizontally scaled deployments, only one instance processes events and schedules at any given time.
Can I trigger a playbook manually on existing data?
Yes. Use the playbookManualExecution GraphQL mutation with the playbook ID and target entity ID. This calls executePlaybookOnEntity in playbookManager.ts, which loads the entity, wraps it in a STIX bundle, and passes it to the executor chain—useful for retroactive enrichment or remediation.
Where are playbook execution logs stored?
Execution metadata, including step outcomes, durations, and errors, is persisted in Redis via the redisPlaybookUpdate function. This enables real-time tracking in the OpenCTI UI and provides audit trails for compliance and debugging purposes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →