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 via playbookStreamHandler
  • cronScheduler: Triggers time-based workflows via handlePlaybookCrons

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_STREAM for event-driven triggers
  • PLAYBOOK_INTERNAL_DATA_CRON for scheduled execution
  • PLAYBOOK_CONNECTOR_COMPONENT for enrichment integrations
  • PLAYBOOK_CONTAINER_WRAPPER_COMPONENT for 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):

  1. playbookStreamHandler receives the SSE event and iterates over cached playbook definitions from getEntitiesListFromCache
  2. 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 configuration filters via isValidEventType
    • Payload satisfies STIX filters via isStixMatchFilterGroup
  3. Upon validation, a STIX bundle is constructed and passed to playbookExecutor
  4. The executor runs the component's executor function, logs the step, and follows links to the next node

Execution Flow for Cron Schedules

Time-based workflows follow a different path (lines 261-361 of playbookManager.ts):

  1. Every minute, the cron handler acquires the lock and invokes handlePlaybookCrons
  2. For playbooks with PLAYBOOK_INTERNAL_DATA_CRON start nodes, shouldTriggerNow evaluates if the current time matches the configured period and triggerTime
  3. When triggered, the system converts filters to query options via convertFiltersToQueryOptions and loads matching entities using stixLoadByFilters or elPaginate
  4. The resulting STIX bundle feeds into the same playbookExecutor pipeline 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: Contains id, component_id, configuration, and position
  • NodeDefinition: 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.ts is 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 playbookAdd mutation
  • Live event processing uses playbookStreamHandler to filter SSE events against playbook configurations and execute matching chains via playbookExecutor
  • Cron workflows trigger periodically through handlePlaybookCrons, building STIX bundles from filtered entity queries
  • Custom components can be added by implementing the PlaybookComponent interface in playbook-components.ts and registering them in the component map
  • Manual execution allows ad-hoc playbook runs against specific entities using the playbookManualExecution GraphQL 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →