# OpenCTI Playbook Manager: How to Create Automated Workflows

> Learn how to create automated workflows with OpenCTI Playbook Manager. Orchestrate STIX events and cron triggers to build powerful security automation.

- Repository: [OpenCTI Platform/opencti](https://github.com/opencti-platform/opencti)
- Tags: how-to-guide
- Published: 2026-02-19

---

**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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`:

```graphql
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`:

```graphql
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:

```graphql
mutation RunPlaybook {
  playbookManualExecution(
    playbook_id: "playbook-id-xyz"
    entity_id: "abcd1234-..."
  )
}

```

This invokes `executePlaybookOnEntity` in [`playbookManager.ts`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts) by implementing the `PlaybookComponent<T>` interface:

```typescript
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`](https://github.com/OpenCTI-Platform/opencti/blob/main/playbook-components.ts) to make it available in the UI palette.

### Programmatic Execution via API

For external automation, trigger playbooks using the OpenCTI client:

```typescript
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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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`](https://github.com/OpenCTI-Platform/opencti/blob/main/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.