Skip to main content

task-manager-registration

Register and schedule background tasks with the Kibana Task Manager plugin (`@kbn/task-manager-plugin`). Use when adding or modifying a task type via `taskManager.registerTaskDefinitions`, calling `ensureScheduled` / `schedule` / `bulkSchedule`, implementing `createTaskRunner` / `cancel`, picking `timeout` / `cost` / `priority` / `maxAttempts`, defining `paramsSchema` / `stateSchemaByVersion`, or reviewing PRs that touch any of these.

Source facts

Repository
elastic/kibana
Last source activity
July 31, 2026 at 15:54
Detected SKILL.md language
English
Stars
21,236
Forks
8,623

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
task-manager-registration
description
Register and schedule background tasks with the Kibana Task Manager plugin (`@kbn/task-manager-plugin`). Use when adding or modifying a task type via `taskManager.registerTaskDefinitions`, calling `ensureScheduled` / `schedule` / `bulkSchedule`, implementing `createTaskRunner` / `cancel`, picking `timeout` / `cost` / `priority` / `maxAttempts`, defining `paramsSchema` / `stateSchemaByVersion`, or reviewing PRs that touch any of these.
# Task Manager — Task Registration & Scheduling > Background tasks run inside the Kibana process and share its capacity pool. A misconfigured task can block shutdown, exhaust the pool, retry forever, or duplicate itself on every restart. The defaults are not always the right choice — verify each field below explicitly. ## Overview A task type is registered in the **setup** lifecycle of a plugin via `taskManager.registerTaskDefinitions({ [type]: definition })`. The definition declares static metadata (`timeout`, `cost`, `priority`, `maxAttempts`, schemas) and a `createTaskRunner` factory that returns `{ run, cancel? }` per task instance. Tasks are **scheduled** separately, usually on plugin **start**, via `taskManager.ensureScheduled` (recurring/idempotent) or `taskManager.schedule` / `bulkSchedule` (one-shot). **Source of truth:** - Definition shape: `x-pack/platform/plugins/shared/task_manager/server/task.ts` (`TaskDefinition`, `RunContext`, `TaskCost`, `TaskPriority`, `InstanceTaskCost`, `DEFAULT_TIMEOUT = '5m'`) - Error helpers: `x-pack/platform/plugins/shared/task_manager/server/task_running/errors.ts` - Plugin entry point exports: `@kbn/task-manager-plugin/server` ## Registration anatomy ```ts taskManager.registerTaskDefinitions({ 'my-plugin:my-task': { title: 'My Task', description: 'What this task does and why it exists.', timeout: '2m', maxAttempts: 1, cost: TaskCost.Normal, priority: TaskPriority.Normal, paramsSchema: schema.object({ /* ... */ }), stateSchemaByVersion: { 1: { schema: stateSchemaV1, up: (s) => s } }, createTaskRunner: (context) => { const { taskInstance, signal } = context; return { run: async () => { /* ... */ }, cancel: async () => { /* cleanup */ }, // optional, see §3 }; }, }, }); ``` Each section below covers one field or concern, the rule, the correct pattern, and the anti-pattern reviewers most commonly flag. ## 1. `timeout` — set it deliberately **Rule:** `timeout` MUST be set explicitly to a value that reflects the task's expected duration plus a reasonable margin. Do NOT inherit the `'5m'` default unless 5 minutes is genuinely the right ceiling. - Too small → Task Manager kills in-flight work and retries; the task never completes. - Too large → A wedged task blocks shutdown and a slot in the capacity pool until the timeout elapses. The validator only checks the format (`{number}{m|s|h|d}`); it does NOT check that the value is sensible. That is the author's responsibility. ```ts // Correct: short tasks get short timeouts { timeout: '30s' } // a quick health scan { timeout: '2m' } // a typical batched job { timeout: '10m' } // a heavier ES migration ``` ```ts // Anti-pattern: defaulting / over-sizing { /* timeout omitted */ } // silently '5m' { timeout: '1h' } // for a task that always finishes in seconds ``` ## 2. `signal` — actually use it **Rule:** `createTaskRunner` MUST destructure `signal` (an `AbortSignal`) from `RunContext` and propagate it to every cancellable operation: ES client calls, `fetch`/HTTP requests, child loops, and any `setTimeout`/`setInterval` based polling. `RunContext` hands the task a `signal` directly — Task Manager owns the underlying `AbortController` internally and never exposes it. Task Manager aborts the signal on timeout or shutdown but never reads `signal.aborted` itself. The signal is the only channel Task Manager has to ask a task to stop — it is the task code's responsibility to comply by passing it to I/O and checking `signal.aborted` in loops. Ignoring it lets in-flight work run past the timeout, hold ES connections open, and block clean shutdown. **A task cannot make itself exit via the signal.** To exit early, return from the top-level task function, or throw an error (Task Manager catches it; classify with the helpers in §4). ### Pass the signal to ES clients ```ts const result = await esClient.search( { index, query, size }, { signal } ); ``` ### Pass the signal to HTTP ```ts await fetch(url, { signal }); ``` ### Bail out of loops ```ts for (const item of items) { if (signal.aborted) return { state }; await processItem(item); } ``` ### Or use a `throwIfAborted` helper ```ts const throwIfAborted = (sig: AbortSignal) => { if (sig.aborted) throw new Error('Task aborted'); }; for (const batch of batches) { throwIfAborted(signal); await processBatch(batch, { signal }); } ``` Throwing is the simpler option, but when `run()` throws, Task Manager persists the state as it was *before* the run (`task_running/task_runner.ts`) — progress the run made is lost. Prefer the `return { state }` form above when a partial run is worth checkpointing. ```ts // Anti-pattern: ignore the signal entirely createTaskRunner: ({ taskInstance }) => ({ run: async () => { for (const id of bigList) { await esClient.search({ index, query: { term: { id } } }); // no signal, no abort check } return { state: {} }; }, }); ``` ## 3. `cancel()` — optional cleanup hook **Rule:** `cancel()` is **optional**, and omitting it is the common case. Add it only when the task holds resources that the abort signal alone does not release: open subscriptions, scheduled timers, file handles, in-memory caches you allocated, or AbortControllers you created downstream. When present, `cancel()` is invoked by Task Manager **on timeout only** — when a task exceeds its `timeout`, the pool calls `cancel()` (`task_pool/task_pool.ts`). On Kibana shutdown, Task Manager stops the poller but does **not** call `cancel()` on tasks that are still running; rely on the abort signal (and your own cleanup paths) for shutdown. `cancel()` runs **alongside** the abort signal — the signal stops in-flight I/O, `cancel()` releases everything else. Do **not** add an empty `cancel: async () => {}` just to satisfy the type. If there is nothing to clean up, omit the field entirely. ```ts createTaskRunner: () => { const subscription = stream$.subscribe(/* ... */); return { run: async () => { /* ... */ }, cancel: async () => { subscription.unsubscribe(); }, }; }; ``` ## 4. Error classification — `throwUnrecoverableError` vs `throwRetryableError` **Rule:** Errors thrown from `run()` MUST be classified. Use `throwUnrecoverableError` for permanent failures (no point retrying) and `throwRetryableError` for transient failures (retry, optionally with custom timing). Generic thrown errors are retried up to `maxAttempts` with default backoff, which is rarely what you want for either case. ```ts import { throwUnrecoverableError, throwRetryableError, } from '@kbn/task-manager-plugin/server'; // Permanent — config missing, feature disabled, malformed params if (!config.endpoint) { throwUnrecoverableError(new Error('Required config xpack.myFeature.endpoint not set')); } // Transient — try again in 60s if (esError.statusCode === 503) { throwRetryableError( new Error('ES temporarily unavailable'), new Date(Date.now() + 60_000) ); } // Transient — try again with default backoff if (lockHeld) { throwRetryableError(new Error('Lock held by another instance'), true); } ``` ```ts // Anti-pattern: throw raw Error for permanent failure if (!config.endpoint) { throw new Error('Required config not set'); // Task will retry maxAttempts times for nothing } ``` ## 5. `paramsSchema` — validate input early **Rule:** Define `paramsSchema` whenever the task accepts `params`. Validation runs at scheduling time, surfacing bad input at the call site instead of inside `run()`. ```ts import { schema } from '@kbn/config-schema'; const paramsSchema = schema.object({ spaceId: schema.string(), ruleIds: schema.arrayOf(schema.string(), { minSize: 1 }), }); taskManager.registerTaskDefinitions({ 'my-plugin:process-rules': { paramsSchema, createTaskRunner: ({ taskInstance }) => ({ run: async () => { // taskInstance.params is now trusted to match paramsSchema }, }), }, }); ``` A task with no params (single-instance recurring tasks) does not need `paramsSchema`. ## 6. `stateSchemaByVersion` — required if state persists between runs **Rule:** Define `stateSchemaByVersion` whenever the task returns non-empty `state` from `run()`. Each version provides a `schema` and an `up` migration from the previous version. State is persisted on the task SO between runs. Without a schema, any state-shape change is silently invalid until it crashes a future `run()` after upgrade. ```ts import { schema } from '@kbn/config-schema'; const stateSchemaV1 = schema.object({ lastRunAt: schema.maybe(schema.string()), }); const stateSchemaV2 = schema.object({ lastRunAt: schema.maybe(schema.string()), cursor: schema.maybe(schema.string()), }); stateSchemaByVersion: { 1: { schema: stateSchemaV1, up: (s) => s }, 2: { schema: stateSchemaV2, up: (s) => ({ ...s, cursor: (s as { cursor?: string }).cursor }), }, } ``` The first run of a task whose state doesn't match the latest schema is migrated forward by chaining `up` functions. `up` MUST be pure and idempotent. If the task is fully stateless (every run starts from scratch), return `{ state: {} }` from `run()` and omit `stateSchemaByVersion`. ## 7. `maxAttempts` — match it to task semantics **Rule:** Set `maxAttempts: 1` for tasks that should NOT retry (one-shot triggers, idempotency-sensitive operations, anything where a duplicate run causes harm). Use the global default (omit) only when retry-on-transient-failure is the desired semantic. ```ts { maxAttempts: 1 } // one-shot: send a notification, kick off a migration { /* omitted */ } // recurring task using framework default + retryable errors ``` ```ts // Anti-pattern: one-shot task with default retries { // sends a Slack message; default maxAttempts means a transient failure // could deliver the message twice } ``` ## 8. `cost` and `priority` — capacity discipline **Rule:** Pick `cost` based on the task's actual resource footprint, not its perceived importance. Pick `priority` only when the task should preempt or yield to others; the default (`Normal`) is correct for most tasks. `TaskCost` values are integers used by the capacity pool: `Tiny = 1`, `Normal = 2`, `Large = 4`, `ExtraLarge = 10`. Capacity is finite; an over-costed task starves its neighbours, an under-costed task gets starved by them. | Cost | Assumed memory budget | When to pick | |---|---|---| | `TaskCost.Tiny` | < 25 MB | Sub-second, no ES query — heartbeats, gauge reporting, light scheduling | | `TaskCost.Normal` | < 50 MB | Default. A handful of ES queries, modest CPU | | `TaskCost.Large` | < 100 MB | Between `Normal` and `ExtraLarge` — a batched job with a moderate footprint | | `TaskCost.ExtraLarge` | < 250 MB | Heavy aggregations, large bulk reads/writes, long-running scans | The memory budgets are the assumption capacity planning is built on; if the task's real footprint exceeds the budget for its tier, bump the cost rather than relying on the smaller tier's slot. | Priority | When to pick | |---|---| | `TaskPriority.Normal` | Default — almost always correct | | `TaskPriority.NormalLongRunning` | Long-running tasks that should not block the regular pool | | `TaskPriority.Low` | Background bookkeeping that may be deferred under load | ### `TaskCost` vs `InstanceTaskCost` These are distinct enums for distinct contexts — confusing them is a recurring review finding (see [PR #260373](https://github.com/elastic/kibana/pull/260373)): - **`TaskCost`** — integer enum (`Tiny = 1`, `Normal = 2`, `Large = 4`, `ExtraLarge = 10`). Use for the **task type definition's** `cost` field and for the **per-instance `cost` override** in `TaskInstance.cost` *when the value is set in code* against the `TaskInstance` type. This is the value the capacity pool reads. - **`InstanceTaskCost`** — string enum (`'tiny'`, `'normal'`, `'large'`, `'extralarge'`). Use whenever cost travels through a **schema or saved-object attribute**: task params, persisted state, anything serialized. Convert to the numeric `TaskCost` with `getTaskCostFromInstance(...)` (exported from `@kbn/task-manager-plugin/server`) before comparing or feeding it back to capacity logic. ```ts import { TaskCost, InstanceTaskCost, getTaskCostFromInstance } from '@kbn/task-manager-plugin/server'; // Task type registration — integer enum { cost: TaskCost.ExtraLarge } // Task params schema — string enum, because it's serialized const paramsSchema = schema.object({ cost: schema.oneOf([ schema.literal(InstanceTaskCost.Tiny), schema.literal(InstanceTaskCost.Normal), schema.literal(InstanceTaskCost.Large), schema.literal(InstanceTaskCost.ExtraLarge), ]), }); // Reading params back at run time const numericCost = getTaskCostFromInstance(taskInstance.params.cost); ``` ## 9. Scheduling — `ensureScheduled` vs `schedule` vs `bulkSchedule` **Rule:** Use `ensureScheduled` for recurring tasks created on plugin startup. Use `schedule` / `bulkSchedule` for one-shot or user-triggered tasks. Misusing `schedule` on startup creates a duplicate task on every Kibana restart. | API | Idempotent? | Use case | |---|---|---| | `ensureScheduled` | Yes — won't create a duplicate if a task with that `id` exists | Recurring system tasks created during plugin start | | `schedule` | No — always creates a new instance | One-shot user-triggered work | | `bulkSchedule` | No — same as `schedule` for many | Bulk one-shot work | `ensureScheduled` is **not** a pure no-op: if a task with the given `id` already exists and the call supplies an interval-based `schedule`, it updates the existing task's schedule to match (`task_scheduling.ts`). Pass an `interval` only when you genuinely want to override whatever schedule is on disk; otherwise fetch the existing task first (see "Preserving user-configured schedules" below) and reuse its `schedule`. ```ts // Correct: recurring task on plugin start public start(core, plugins) { void plugins.taskManager.ensureScheduled({
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub