Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c6ff65f
Introduce an event bus
cnasikas May 19, 2026
6d07213
Event bus improvements
cnasikas May 19, 2026
4eaaa3c
Add unit tests
cnasikas May 19, 2026
80ac903
Merge branch 'main' into alerting_v2_event_bus
cnasikas May 19, 2026
c09ec2d
Changes from make api-docs
kibanamachine May 19, 2026
cd3fefd
Merge branch 'main' into alerting_v2_event_bus
cnasikas May 20, 2026
e43a1b4
Switch to setImmediate and EventEmitterAsyncResource
cnasikas May 20, 2026
9afde85
PR feedback
cnasikas May 20, 2026
28a9188
Create alert action publisher
cnasikas May 20, 2026
0f8e5c0
Fix TS errors
cnasikas May 20, 2026
068fedd
Merge branch 'alerting_v2_event_bus' into alerting-v2-workflow-trigge…
cnasikas May 20, 2026
2714f6f
Create the alert action pub/sub
cnasikas May 21, 2026
0c4978c
Adapting to the new bus.
adcoelho May 21, 2026
1dd5101
Merge remote-tracking branch 'upstream/main' into alerting-v2-workflo…
adcoelho May 22, 2026
94de798
Changes from node scripts/check
kibanamachine May 22, 2026
c06e33b
Type fixes
adcoelho May 22, 2026
741b21f
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
adcoelho May 22, 2026
e7a671b
fix test
adcoelho May 22, 2026
fe5bd86
Add logger.
adcoelho May 22, 2026
ee0a69f
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
adcoelho May 22, 2026
3aafc90
updating limits
adcoelho May 22, 2026
7a15f24
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
adcoelho May 22, 2026
a6569f5
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
cnasikas May 25, 2026
a45ed7f
Fix limits
cnasikas May 25, 2026
0a9a061
PR feedback
cnasikas May 25, 2026
1a60880
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
cnasikas May 28, 2026
19b1d78
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
elasticmachine May 29, 2026
c28445c
Merge branch 'main' into alerting-v2-workflow-trigger-assign-episode
cnasikas Jun 2, 2026
7a89d3a
Change trigger ID
cnasikas Jun 2, 2026
4aaa6dc
Add limits to the schema
cnasikas Jun 2, 2026
d8a8114
Merge branch 'alerting-v2-workflow-trigger-assign-episode' of github.…
cnasikas Jun 2, 2026
11fbf49
Fix unit test
cnasikas Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pageLoadAssetSize:
aiAssistantManagementSelection: 11569
aiops: 15227
alerting: 22371
alertingVTwo: 51297
alertingVTwo: 56591
apm: 54371
apmSourcesAccess: 2278
automaticImport: 18629
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
* and copy the schemaHash from the response for the trigger id.
*/
export const APPROVED_TRIGGER_DEFINITIONS: Array<{ id: string; schemaHash: string }> = [
{
id: 'alertingV2.episodeAssigned',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably need to change this as well

schemaHash: 'fce48752c788e4620a70cf8d33040117c7a46c78508a5cf98c5c0fa93f631ad0',
},
{
id: 'cases.caseCreated',
schemaHash: '5b562db9463664a1e28ff2f3ee7edec229e83912569190cd8a83f53d38da9ed8',
Expand Down
2 changes: 1 addition & 1 deletion x-pack/platform/plugins/shared/alerting_v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ If you want implementation detail for one subsystem, continue with:
- API shape or saved object contracts: inspect `server/routes/` and `server/saved_objects/` together with the relevant subsystem docs
- Workflow triggers (workflows_extensions registration + runtime emission): see the public and server READMEs
- [`public/lib/workflow_extensions/README.md`](public/lib/workflow_extensions/README.md)
- [`server/lib/services/workflow_extensions_service/README.md`](server/lib/services/workflow_extensions_service/README.md)
- [`server/lib/services/workflow_service/README.md`](server/lib/services/workflow_service/README.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';
import { z } from '@kbn/zod/v4';
import type { CommonTriggerDefinition } from '@kbn/workflows-extensions/common';

export const EPISODE_ASSIGNED_TRIGGER_ID = 'alerting.episodeAssigned' as const;

export const episodeAssignedPayloadSchema = z
.object({
occurredAt: z.iso.datetime().describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.occurredAt', {
defaultMessage: 'ISO timestamp of when the assignment occurred.',
})
),
groupHash: z
.string()
.min(1)
.max(128)
.describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.groupHash', {
defaultMessage: 'Stable hash of the alert grouping the episode belongs to.',
})
),
episodeId: z
.string()
.min(1)
.max(256)
.describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.episodeId', {
defaultMessage: 'Identifier of the alerting episode whose assignee changed.',
})
),
ruleId: z
.string()
.min(1)
.max(256)
.describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.ruleId', {
defaultMessage: 'Identifier of the alerting rule the episode belongs to.',
})
),
spaceId: z
.string()
.min(1)
.max(256)
.describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.spaceId', {
defaultMessage: 'Kibana space the episode lives in.',
})
),
actorUid: z
.string()
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
.min(1)
.max(256)
.nullable()
.describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.actorUid', {
defaultMessage:
'User-profile uid of the actor who changed the assignee, or null when performed by an internal/system context.',
})
),
assigneeUid: z
.string()
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
.min(1)
.max(256)
.nullable()
.describe(
i18n.translate('xpack.alertingVTwo.triggers.episodeAssigned.schema.assigneeUid', {
defaultMessage:
'User-profile uid of the new assignee, or null when the episode was unassigned.',
})
),
})
.strict();

export type EpisodeAssignedPayload = z.infer<typeof episodeAssignedPayloadSchema>;

export const episodeAssignedTriggerCommonDefinition: CommonTriggerDefinition<
typeof episodeAssignedPayloadSchema
> = {
id: EPISODE_ASSIGNED_TRIGGER_ID,
eventSchema: episodeAssignedPayloadSchema,
title: i18n.translate('xpack.alertingVTwo.workflowTriggers.episodeAssigned.title', {
defaultMessage: 'Alerting - Episode assigned',
}),
description: i18n.translate('xpack.alertingVTwo.workflowTriggers.episodeAssigned.description', {
defaultMessage: 'Emitted when an alerting episode is assigned to a user.',
}),
documentation: {
details: i18n.translate(
'xpack.alertingVTwo.workflowTriggers.episodeAssigned.documentation.details',
{
defaultMessage:
'Emitted after an episode assign action is persisted with a non-null assignee. The payload includes event.episodeId, event.ruleId, event.spaceId, and event.assigneeUid for trigger conditions.',
}
),
examples: [
i18n.translate('xpack.alertingVTwo.workflowTriggers.episodeAssigned.documentation.example', {
defaultMessage: `## Run for a specific rule
\`\`\`yaml
triggers:
- type: {triggerId}
on:
condition: 'event.ruleId: "my-rule-id"'
\`\`\``,
values: {
triggerId: EPISODE_ASSIGNED_TRIGGER_ID,
},
}),
],
},
snippets: {
condition: 'event.assigneeUid: "user-profile-uid"',
},
};
Comment thread
cnasikas marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export {
EPISODE_ASSIGNED_TRIGGER_ID,
episodeAssignedPayloadSchema,
episodeAssignedTriggerCommonDefinition,
} from './episode_assigned';
export type { EpisodeAssignedPayload } from './episode_assigned';
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This folder owns the **public-side (browser) registration** of `alerting_v2` wor
- **Public setup contract** (`WorkflowsExtensionsPublicPluginSetup`) — UI metadata for the Workflows builder (title, description, icon, docs, snippets). This is what controls discoverability of a trigger in the Workflows UI.
- **Server setup contract** (`WorkflowsExtensionsServerPluginSetup`) — runtime validation/execution of triggers and steps.

This README covers the **public** side. For the server side (where runtime emission also lives), see [`server/lib/services/workflow_extensions_service/README.md`](../../../server/lib/services/workflow_extensions_service/README.md).
This README covers the **public** side. For the server side (where runtime emission also lives), see [`server/lib/services/workflow_service/README.md`](../../../server/lib/services/workflow_service/README.md).

## Registering a new trigger

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
* 2.0.
*/

import type {
PublicTriggerDefinition,
WorkflowsExtensionsPublicPluginSetup,
} from '@kbn/workflows-extensions/public';
import type { WorkflowsExtensionsPublicPluginSetup } from '@kbn/workflows-extensions/public';
import { episodeAssignedTriggerPublicDefinition } from './triggers/episode_assigned';

/**
* Registers all alerting-v2 public workflow trigger definitions (UI metadata).
Expand All @@ -17,11 +15,5 @@ import type {
export function registerTriggerDefinitions(
workflowsExtensions: WorkflowsExtensionsPublicPluginSetup
): void {
const triggerDefinitions: PublicTriggerDefinition[] = [
// Add PublicTriggerDefinition entries here (spread common id + eventSchema + title, icon, docs).
];

for (const definition of triggerDefinitions) {
workflowsExtensions.registerTriggerDefinition(definition);
}
workflowsExtensions.registerTriggerDefinition(episodeAssignedTriggerPublicDefinition);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public';
import { episodeAssignedTriggerCommonDefinition } from '../../../../common/workflows/triggers';

const EpisodeAssignedIcon = React.lazy(() =>
// @ts-expect-error EUI does not ship `.d.ts` files for deep `icon/assets/*`
// subpaths. Other plugins work around this with an ambient `eui_icons.d.ts`
// (see e.g. x-pack/platform/plugins/shared/cases/public/workflows/eui_icons.d.ts).
import('@elastic/eui/es/components/icon/assets/user').then(({ icon }) => ({
default: icon,
}))
);

export const episodeAssignedTriggerPublicDefinition: PublicTriggerDefinition = {
...episodeAssignedTriggerCommonDefinition,
icon: EpisodeAssignedIcon,
};
2 changes: 2 additions & 0 deletions x-pack/platform/plugins/shared/alerting_v2/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { bindOnSetup } from './setup/bind_on_setup';
import { bindOnStart } from './setup/bind_on_start';
import { bindRoutes } from './setup/bind_routes';
import { bindServices } from './setup/bind_services';
import { bindEvents } from './setup/bind_events';
import { bindRuleExecutionServices } from './setup/bind_rule_executor';
import { bindDispatcherExecutionServices } from './setup/bind_dispatcher_executor';
import { bindTasks } from './setup/bind_tasks';
Expand All @@ -28,6 +29,7 @@ export const module = new ContainerModule((options) => {
bindContract(options);
bindRoutes(options);
bindServices(options);
bindEvents(options);
bindRuleExecutionServices(options);
bindDispatcherExecutionServices(options);
bindTasks(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import type { ElasticsearchClient } from '@kbn/core/server';
import type { UserProfileServiceStart } from '@kbn/core-user-profile-server';
import type { DeeplyMockedApi } from '@kbn/core-elasticsearch-client-server-mocks';
import { httpServerMock } from '@kbn/core-http-server-mocks';
import type { PublicMethodsOf } from '@kbn/utility-types';
import { createAlertActionEventPublisher } from '../events/alert_action_event_publisher/alert_action_event_publisher.mock';
import type { AlertActionEventPublisher } from '../events/alert_action_event_publisher/alert_action_event_publisher';
import { createQueryService } from '../services/query_service/query_service.mock';
import { createStorageService } from '../services/storage_service/storage_service.mock';
import { createUserService, createUserProfile } from '../services/user_service/user_service.mock';
Expand All @@ -19,21 +22,32 @@ export function createAlertActionsClient(): {
queryServiceEsClient: DeeplyMockedApi<ElasticsearchClient>;
storageServiceEsClient: jest.Mocked<ElasticsearchClient>;
userProfileService: jest.Mocked<UserProfileServiceStart>;
alertActionEventPublisher: AlertActionEventPublisher;
} {
const { queryService, mockEsClient: queryServiceEsClient } = createQueryService();
const { storageService, mockEsClient: storageServiceEsClient } = createStorageService();
const { userService, userProfileService } = createUserService();
const { publisher: alertActionEventPublisher } = createAlertActionEventPublisher();
const request = httpServerMock.createKibanaRequest();

userProfileService.getCurrent.mockResolvedValue(createUserProfile('test-uid'));

const alertActionsClient = new AlertActionsClient(
queryService,
storageService,
userService,
'default'
request,
'default',
alertActionEventPublisher
);

return { alertActionsClient, queryServiceEsClient, storageServiceEsClient, userProfileService };
return {
alertActionsClient,
queryServiceEsClient,
storageServiceEsClient,
userProfileService,
alertActionEventPublisher,
};
}

export function createAlertActionsClientMock(): jest.Mocked<PublicMethodsOf<AlertActionsClient>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { UserProfileServiceStart } from '@kbn/core-user-profile-server';
import type { DeeplyMockedApi } from '@kbn/core-elasticsearch-client-server-mocks';
import type { BulkCreateAlertActionItemBody } from '@kbn/alerting-v2-schemas';
import { ALERT_EPISODE_ACTION_TYPE, type CreateAlertActionBody } from '@kbn/alerting-v2-schemas';
import type { AlertActionEventPublisher } from '../events/alert_action_event_publisher/alert_action_event_publisher';
import type { AlertActionsClient } from './alert_actions_client';
import { createAlertActionsClient } from './alert_actions_client.mock';
import {
Expand All @@ -24,14 +25,18 @@ describe('AlertActionsClient', () => {
let queryServiceEsClient: DeeplyMockedApi<ElasticsearchClient>;
let storageServiceEsClient: jest.Mocked<ElasticsearchClient>;
let userProfileService: jest.Mocked<UserProfileServiceStart>;
let alertActionEventPublisher: AlertActionEventPublisher;
let emitEpisodeActionsSpy: jest.SpyInstance;

beforeEach(() => {
({
alertActionsClient: client,
queryServiceEsClient,
storageServiceEsClient,
userProfileService,
alertActionEventPublisher,
} = createAlertActionsClient());
emitEpisodeActionsSpy = jest.spyOn(alertActionEventPublisher, 'emitEpisodeActions');
storageServiceEsClient.bulk.mockResolvedValueOnce({ items: [], errors: false, took: 1 });
});

Expand Down Expand Up @@ -216,6 +221,83 @@ describe('AlertActionsClient', () => {
});
});

describe('episode action domain events', () => {
it('calls emitEpisodeActions with the persisted assign action document', async () => {
queryServiceEsClient.esql.query.mockResolvedValueOnce(getAlertEventESQLResponse());

await client.createAction({
groupHash: 'test-group-hash',
action: {
action_type: ALERT_EPISODE_ACTION_TYPE.ASSIGN,
episode_id: 'episode-1',
assignee_uid: 'assignee-uid-1',
},
});

expect(emitEpisodeActionsSpy).toHaveBeenCalledTimes(1);
expect(emitEpisodeActionsSpy).toHaveBeenCalledWith(expect.anything(), [
expect.objectContaining({
action_type: ALERT_EPISODE_ACTION_TYPE.ASSIGN,
assignee_uid: 'assignee-uid-1',
episode_id: 'episode-1',
group_hash: 'test-group-hash',
actor: 'test-uid',
}),
]);
});

it('does not call emitEpisodeActions when persistence fails', async () => {
queryServiceEsClient.esql.query.mockResolvedValueOnce(getEmptyESQLResponse());

await expect(
client.createAction({
groupHash: 'unknown-group-hash',
action: {
action_type: ALERT_EPISODE_ACTION_TYPE.ASSIGN,
episode_id: 'episode-1',
assignee_uid: 'assignee-uid-1',
},
})
).rejects.toThrow();

expect(emitEpisodeActionsSpy).not.toHaveBeenCalled();
});

it('calls emitEpisodeActions for bulk assign actions only among persisted docs', async () => {
const actions: BulkCreateAlertActionItemBody[] = [
{
group_hash: 'group-hash-1',
action_type: ALERT_EPISODE_ACTION_TYPE.ASSIGN,
episode_id: 'episode-1',
assignee_uid: 'assignee-uid-1',
},
{
group_hash: 'group-hash-2',
action_type: ALERT_EPISODE_ACTION_TYPE.ACK,
episode_id: 'episode-2',
},
];

queryServiceEsClient.esql.query.mockResolvedValueOnce(
getBulkAlertEventsESQLResponse([
{ group_hash: 'group-hash-1', episode_id: 'episode-1' },
{ group_hash: 'group-hash-2', episode_id: 'episode-2' },
])
);

await client.createBulkActions(actions);

expect(emitEpisodeActionsSpy).toHaveBeenCalledTimes(1);
expect(emitEpisodeActionsSpy.mock.calls[0][1]).toHaveLength(2);
expect(emitEpisodeActionsSpy.mock.calls[0][1][0]).toMatchObject({
action_type: ALERT_EPISODE_ACTION_TYPE.ASSIGN,
});
expect(emitEpisodeActionsSpy.mock.calls[0][1][1]).toMatchObject({
action_type: ALERT_EPISODE_ACTION_TYPE.ACK,
});
});
});

describe('error codes and details', () => {
it('attaches ALERT_EVENT_NOT_FOUND code with group_hash and episode_id details on createAction', async () => {
queryServiceEsClient.esql.query.mockResolvedValueOnce(getEmptyESQLResponse());
Expand Down
Loading
Loading