diff --git a/examples/workflows_extensions_example/common/triggers/custom_trigger.ts b/examples/workflows_extensions_example/common/triggers/custom_trigger.ts index 97145fa5b7cc4..2142bce316040 100644 --- a/examples/workflows_extensions_example/common/triggers/custom_trigger.ts +++ b/examples/workflows_extensions_example/common/triggers/custom_trigger.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { i18n } from '@kbn/i18n'; import { z } from '@kbn/zod/v4'; import type { CommonTriggerDefinition } from '@kbn/workflows-extensions/common'; @@ -34,8 +35,69 @@ export const customTriggerEventSchema = z.object({ export type CustomTriggerEvent = z.infer; -/** Shared trigger definition (id + eventSchema) for use by public and server. */ +/** Shared trigger definition for use by public and server. */ export const commonCustomTriggerDefinition: CommonTriggerDefinition = { id: CUSTOM_TRIGGER_ID, eventSchema: customTriggerEventSchema, + title: i18n.translate('workflowsExtensionsExample.customTrigger.title', { + defaultMessage: 'Custom trigger', + }), + description: i18n.translate('workflowsExtensionsExample.customTrigger.description', { + defaultMessage: + 'Emitted when a custom event occurs. Used by the workflows extensions example plugin.', + }), + documentation: { + details: i18n.translate('workflowsExtensionsExample.customTrigger.documentation.details', { + defaultMessage: + 'Emitted when a custom event occurs. Events can include an optional category (e.g. alerts, notifications, audit, demo). In the `on` block, add a condition (KQL) to filter when this workflow runs using event properties: `event.category`, `event.message`, `event.source`.', + }), + examples: [ + i18n.translate( + 'workflowsExtensionsExample.customTrigger.documentation.exampleMatchCategory', + { + defaultMessage: `## Match by category (conditional subscription) +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.category: "alerts"' +\`\`\``, + values: { triggerId: CUSTOM_TRIGGER_ID }, + } + ), + i18n.translate('workflowsExtensionsExample.customTrigger.documentation.exampleMatchMessage', { + defaultMessage: `## Match any message +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.message: *' +\`\`\``, + values: { triggerId: CUSTOM_TRIGGER_ID }, + }), + i18n.translate('workflowsExtensionsExample.customTrigger.documentation.exampleMatchSource', { + defaultMessage: `## Match events from a specific source +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.source: "api"' +\`\`\``, + values: { triggerId: CUSTOM_TRIGGER_ID }, + }), + i18n.translate('workflowsExtensionsExample.customTrigger.documentation.exampleMatchError', { + defaultMessage: `## Match message containing "error" +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.message: *error*' +\`\`\``, + values: { triggerId: CUSTOM_TRIGGER_ID }, + }), + ], + }, + snippets: { + condition: 'event.category: "alerts"', + }, }; diff --git a/examples/workflows_extensions_example/common/triggers/loop_trigger.ts b/examples/workflows_extensions_example/common/triggers/loop_trigger.ts index 3799c9fa4f153..5b24e29f9dcf1 100644 --- a/examples/workflows_extensions_example/common/triggers/loop_trigger.ts +++ b/examples/workflows_extensions_example/common/triggers/loop_trigger.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { i18n } from '@kbn/i18n'; import { z } from '@kbn/zod/v4'; import type { CommonTriggerDefinition } from '@kbn/workflows-extensions/common'; @@ -20,8 +21,35 @@ export const loopTriggerEventSchema = z.object({ export type LoopTriggerEvent = z.infer; -/** Shared trigger definition (id + eventSchema) for use by public and server. */ +/** Shared trigger definition for use by public and server. */ export const commonLoopTriggerDefinition: CommonTriggerDefinition = { id: LOOP_TRIGGER_ID, eventSchema: loopTriggerEventSchema, + title: i18n.translate('workflowsExtensionsExample.loopTrigger.title', { + defaultMessage: 'Loop trigger', + }), + description: i18n.translate('workflowsExtensionsExample.loopTrigger.description', { + defaultMessage: + 'Emitted for the event-chain depth demo. Start or re-emit via POST to /api/workflows_extensions_example/emit_loop.', + }), + documentation: { + details: i18n.translate('workflowsExtensionsExample.loopTrigger.documentation.details', { + defaultMessage: + 'Used to demonstrate the event-chain depth guardrail (workflowsExecutionEngine.eventDriven.maxChainDepth, default 10). Emit via the emit_loop endpoint; a workflow should use a kibana.request step to POST back to that endpoint with the next iteration so chain depth headers propagate until the guardrail stops scheduling.', + }), + examples: [ + i18n.translate('workflowsExtensionsExample.loopTrigger.documentation.exampleStart', { + defaultMessage: `## Start the loop +\`\`\`bash +curl -X POST -u elastic:changeme -H 'Content-Type: application/json' \\ + 'http://localhost:5601/api/workflows_extensions_example/emit_loop' \\ + -d '{}' +\`\`\` +(iteration defaults to 0.)`, + }), + ], + }, + snippets: { + condition: '', + }, }; diff --git a/examples/workflows_extensions_example/public/triggers/custom_trigger.ts b/examples/workflows_extensions_example/public/triggers/custom_trigger.ts index 8c8249ae4a71c..472f82f7be8ee 100644 --- a/examples/workflows_extensions_example/public/triggers/custom_trigger.ts +++ b/examples/workflows_extensions_example/public/triggers/custom_trigger.ts @@ -7,78 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { i18n } from '@kbn/i18n'; -import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; import React from 'react'; -import { - CUSTOM_TRIGGER_ID, - commonCustomTriggerDefinition, -} from '../../common/triggers/custom_trigger'; +import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; +import { commonCustomTriggerDefinition } from '../../common/triggers/custom_trigger'; export const customTriggerPublicDefinition: PublicTriggerDefinition = { ...commonCustomTriggerDefinition, - title: i18n.translate('workflowsExtensionsExample.customTrigger.title', { - defaultMessage: 'Custom trigger', - }), - description: i18n.translate('workflowsExtensionsExample.customTrigger.description', { - defaultMessage: - 'Emitted when a custom event occurs. Used by the workflows extensions example plugin.', - }), icon: React.lazy(() => import('@elastic/eui/es/components/icon/assets/star').then(({ icon }) => ({ default: icon })) ), - documentation: { - details: i18n.translate('workflowsExtensionsExample.customTrigger.documentation.details', { - defaultMessage: - 'Emitted when a custom event occurs. Events can include an optional category (e.g. alerts, notifications, audit, demo). In the `on` block, add a condition (KQL) to filter when this workflow runs using event properties: `event.category`, `event.message`, `event.source`.', - }), - examples: [ - i18n.translate( - 'workflowsExtensionsExample.customTrigger.documentation.exampleMatchCategory', - { - defaultMessage: `## Match by category (conditional subscription) -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.category: "alerts"' -\`\`\``, - values: { triggerId: CUSTOM_TRIGGER_ID }, - } - ), - i18n.translate('workflowsExtensionsExample.customTrigger.documentation.exampleMatchMessage', { - defaultMessage: `## Match any message -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.message: *' -\`\`\``, - values: { triggerId: CUSTOM_TRIGGER_ID }, - }), - i18n.translate('workflowsExtensionsExample.customTrigger.documentation.exampleMatchSource', { - defaultMessage: `## Match events from a specific source -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.source: "api"' -\`\`\``, - values: { triggerId: CUSTOM_TRIGGER_ID }, - }), - i18n.translate('workflowsExtensionsExample.customTrigger.documentation.exampleMatchError', { - defaultMessage: `## Match message containing "error" -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.message: *error*' -\`\`\``, - values: { triggerId: CUSTOM_TRIGGER_ID }, - }), - ], - }, - snippets: { - condition: 'event.category: "alerts"', - }, }; diff --git a/examples/workflows_extensions_example/public/triggers/loop_trigger.ts b/examples/workflows_extensions_example/public/triggers/loop_trigger.ts index d1dd0964d7c49..3585d84c8ed0e 100644 --- a/examples/workflows_extensions_example/public/triggers/loop_trigger.ts +++ b/examples/workflows_extensions_example/public/triggers/loop_trigger.ts @@ -7,41 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { i18n } from '@kbn/i18n'; -import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; import React from 'react'; +import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; import { commonLoopTriggerDefinition } from '../../common/triggers/loop_trigger'; export const loopTriggerPublicDefinition: PublicTriggerDefinition = { ...commonLoopTriggerDefinition, - title: i18n.translate('workflowsExtensionsExample.loopTrigger.title', { - defaultMessage: 'Loop trigger', - }), - description: i18n.translate('workflowsExtensionsExample.loopTrigger.description', { - defaultMessage: - 'Emitted for the event-chain depth demo. Start or re-emit via POST to /api/workflows_extensions_example/emit_loop.', - }), icon: React.lazy(() => import('@elastic/eui/es/components/icon/assets/refresh').then(({ icon }) => ({ default: icon })) ), - documentation: { - details: i18n.translate('workflowsExtensionsExample.loopTrigger.documentation.details', { - defaultMessage: - 'Used to demonstrate the event-chain depth guardrail (workflowsExecutionEngine.eventDriven.maxChainDepth, default 10). Emit via the emit_loop endpoint; a workflow should use a kibana.request step to POST back to that endpoint with the next iteration so chain depth headers propagate until the guardrail stops scheduling.', - }), - examples: [ - i18n.translate('workflowsExtensionsExample.loopTrigger.documentation.exampleStart', { - defaultMessage: `## Start the loop -\`\`\`bash -curl -X POST -u elastic:changeme -H 'Content-Type: application/json' \\ - 'http://localhost:5601/api/workflows_extensions_example/emit_loop' \\ - -d '{}' -\`\`\` -(iteration defaults to 0.)`, - }), - ], - }, - snippets: { - condition: '', - }, }; diff --git a/src/platform/plugins/shared/workflows_extensions/README.md b/src/platform/plugins/shared/workflows_extensions/README.md index f4ecf10bc76e7..5f8de21abe7e9 100644 --- a/src/platform/plugins/shared/workflows_extensions/README.md +++ b/src/platform/plugins/shared/workflows_extensions/README.md @@ -30,8 +30,8 @@ This separation ensures that: The trigger registry follows the same pattern as steps: -- **Server-side registry**: Stores trigger definitions (id + `eventSchema` for payload validation). Other plugins register during `setup()`. -- **Public-side registry**: Stores UI definition (title, description, icon, documentation, snippets) so the workflows UI can display triggers and help users subscribe. +- **Server-side registry**: Stores trigger definitions (`id`, `eventSchema`, title, description, documentation, snippets). Other plugins register the shared **common** definition during `setup()`. +- **Public-side registry**: Spreads the same common definition and adds **icon** (and optional async loader) for the workflows UI. **Async registration (public only):** The public trigger registry accepts either a definition or a **loader function** `() => Promise`. Using a loader (e.g. `() => import('./my_trigger').then(m => m.myTriggerDefinition)`) keeps trigger modules and heavy deps out of your plugin's main bundle. Loaders are resolved in the background; `workflowsExtensions.isReady()` waits for both step and trigger loaders before the workflows UI renders. diff --git a/src/platform/plugins/shared/workflows_extensions/common/index.ts b/src/platform/plugins/shared/workflows_extensions/common/index.ts index 473b9c912c336..1959d04b60b8d 100644 --- a/src/platform/plugins/shared/workflows_extensions/common/index.ts +++ b/src/platform/plugins/shared/workflows_extensions/common/index.ts @@ -8,7 +8,11 @@ */ export type { CommonStepDefinition } from './step_registry/types'; -export type { CommonTriggerDefinition } from './trigger_registry/types'; +export type { + CommonTriggerDefinition, + TriggerDocumentation, + TriggerSnippets, +} from './trigger_registry/types'; export { EVENT_FIELD_PREFIX } from './trigger_registry/constants'; export { DataMapStepTypeId, diff --git a/src/platform/plugins/shared/workflows_extensions/common/trigger_registry/types.ts b/src/platform/plugins/shared/workflows_extensions/common/trigger_registry/types.ts index 92ac48d94d510..92c31cef2f7fd 100644 --- a/src/platform/plugins/shared/workflows_extensions/common/trigger_registry/types.ts +++ b/src/platform/plugins/shared/workflows_extensions/common/trigger_registry/types.ts @@ -9,15 +9,44 @@ import type { z } from '@kbn/zod/v4'; +/** + * Documentation for a trigger (aligned with steps: details + examples). + */ +export interface TriggerDocumentation { + /** + * Detailed description with usage examples (markdown supported) + */ + details?: string; + /** + * Usage examples as markdown strings (e.g. "## Title\n```yaml\n..."). + */ + examples?: string[]; +} + +/** + * Pre-filled snippet values when the user adds this trigger from the UI. + */ +export interface TriggerSnippets { + /** + * KQL condition pre-filled in the trigger's `on.condition` when the user adds this + * trigger from the UI (actions menu or YAML autocomplete). + * Must be valid KQL and only reference properties from the event schema (validated at registration). + */ + condition?: string; +} + /** * Shared trigger contract (common to server and public). * * Constraints (enforced at registration): * - id: globally unique, namespaced format . * - eventSchema: must be a Zod object schema that rejects unknown fields + * + * Server and agent tooling read title, description, documentation, and snippets from here. + * Public definitions spread this object and add UI-only fields (e.g. icon). */ export interface CommonTriggerDefinition { - /** Globally unique, namespaced identifier (e.g. cases.updated, alerts.severity_high) */ + /** Globally unique, namespaced identifier (e.g. cases.updated, alerts.recovered) */ id: string; /** * Payload contract (Zod object schema; must reject unknown fields by default). @@ -25,4 +54,20 @@ export interface CommonTriggerDefinition import('...')), * } */ export interface PublicTriggerDefinition extends CommonTriggerDefinition { - /** - * Short human-readable name for this trigger. - * Displayed in the UI when selecting or viewing triggers. - */ - title: string; - - /** - * User-facing description of when this trigger is emitted. - * Displayed as help text or in tooltips. - */ - description: string; - /** * Used to visually represent this trigger in the UI. */ icon?: React.ComponentType; - - /** - * Documentation (details + examples), aligned with step definitions. - */ - documentation?: TriggerDocumentation; - - /** - * Pre-filled values for snippet insertion (e.g. on.condition). - */ - snippets?: TriggerSnippets; } diff --git a/src/platform/plugins/shared/workflows_extensions/public/triggers/workflow_execution_failed.ts b/src/platform/plugins/shared/workflows_extensions/public/triggers/workflow_execution_failed.ts index 52e3ff79ba959..5e3a6b4206d4c 100644 --- a/src/platform/plugins/shared/workflows_extensions/public/triggers/workflow_execution_failed.ts +++ b/src/platform/plugins/shared/workflows_extensions/public/triggers/workflow_execution_failed.ts @@ -8,87 +8,12 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { - commonWorkflowExecutionFailedTriggerDefinition, - WORKFLOW_EXECUTION_FAILED_TRIGGER_ID, -} from '../../common/triggers/workflow_execution_failed'; +import { commonWorkflowExecutionFailedTriggerDefinition } from '../../common/triggers/workflow_execution_failed'; import type { PublicTriggerDefinition } from '../trigger_registry/types'; export const workflowExecutionFailedPublicTriggerDefinition: PublicTriggerDefinition = { ...commonWorkflowExecutionFailedTriggerDefinition, - title: i18n.translate('workflowsExtensions.triggers.workflowExecutionFailed.title', { - defaultMessage: 'Workflow failed', - }), - description: i18n.translate('workflowsExtensions.triggers.workflowExecutionFailed.description', { - defaultMessage: - 'Emitted when any workflow in the same space fails. Use to handle errors, send notifications, perform cleanup, or retry failed operations.', - }), icon: React.lazy(() => import('@elastic/eui/es/components/icon/assets/error').then(({ icon }) => ({ default: icon })) ), - documentation: { - details: i18n.translate( - 'workflowsExtensions.triggers.workflowExecutionFailed.documentation.details', - { - defaultMessage: `Emitted when a workflow run fails. The event includes \`workflow\` (id, name, spaceId, isErrorHandler), \`execution\` (id, startedAt, failedAt), and \`error\` (message, stepId, stepName, stepExecutionId). Use KQL in \`on.condition\` to filter by workflow name, failed step, or exclude error-handler workflows to avoid infinite loops.`, - } - ), - examples: [ - i18n.translate( - 'workflowsExtensions.triggers.workflowExecutionFailed.documentation.exampleBasic', - { - defaultMessage: `## Log all workflow failures -\`\`\`yaml -triggers: - - type: {triggerId} -steps: - - name: log_to_index - type: elasticsearch.index - with: - index: workflow-errors - body: - workflow_id: "{eventWorkflowId}" - error: "{eventErrorMessage}" - timestamp: "{eventFailedAt}" -\`\`\``, - values: { - triggerId: WORKFLOW_EXECUTION_FAILED_TRIGGER_ID, - eventWorkflowId: '{{event.workflow.id}}', - eventErrorMessage: '{{event.error.message}}', - eventFailedAt: '{{event.execution.failedAt}}', - }, - } - ), - i18n.translate( - 'workflowsExtensions.triggers.workflowExecutionFailed.documentation.exampleFilterName', - { - defaultMessage: `## Filter by workflow name -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: event.workflow.name: critical* -\`\`\``, - values: { triggerId: WORKFLOW_EXECUTION_FAILED_TRIGGER_ID }, - } - ), - i18n.translate( - 'workflowsExtensions.triggers.workflowExecutionFailed.documentation.exampleExcludeErrorHandlers', - { - defaultMessage: `## Exclude error-handler workflows (prevent loops) -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: not event.workflow.isErrorHandler:true -\`\`\``, - values: { triggerId: WORKFLOW_EXECUTION_FAILED_TRIGGER_ID }, - } - ), - ], - }, - snippets: { - condition: 'not event.workflow.isErrorHandler:true', - }, }; diff --git a/src/platform/plugins/shared/workflows_management/common/build_trigger_definitions_for_agent.test.ts b/src/platform/plugins/shared/workflows_management/common/build_trigger_definitions_for_agent.test.ts new file mode 100644 index 0000000000000..7777a3d0c2a6f --- /dev/null +++ b/src/platform/plugins/shared/workflows_management/common/build_trigger_definitions_for_agent.test.ts @@ -0,0 +1,177 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { builtInTriggerDefinitions } from '@kbn/workflows'; +import { commonWorkflowExecutionFailedTriggerDefinition } from '@kbn/workflows-extensions/common'; +import { z } from '@kbn/zod/v4'; +import { + isTriggerDefinitionsLookupError, + lookupTriggerDefinitionsForAgent, +} from './build_trigger_definitions_for_agent'; + +const mockCasesTrigger = { + id: 'cases.caseUpdated', + eventSchema: z.object({ + caseId: z.string(), + owner: z.string(), + updatedFields: z.array(z.string()).optional(), + }), + title: 'Cases - Case updated', + description: 'Emitted when a case is updated.', + documentation: { + examples: [ + `triggers: + - type: cases.caseUpdated + on: + condition: 'event.owner: "securitySolution"'`, + ], + }, +}; + +describe('lookupTriggerDefinitionsForAgent', () => { + it('returns built-in and registered trigger types without filter', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [mockCasesTrigger], + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + expect(result.count).toBe(builtInTriggerDefinitions.length + 1); + expect(result.triggerTypes.map((t) => t.id)).toContain('cases.caseUpdated'); + expect(result.triggerTypes.map((t) => t.id)).toContain('manual'); + }); + + it('returns jsonSchema and examples for built-in triggers', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [mockCasesTrigger], + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + const builtInTriggers = result.triggerTypes.filter((trigger) => + builtInTriggerDefinitions.some((def) => def.id === trigger.id) + ); + for (const trigger of builtInTriggers) { + expect(trigger.jsonSchema).toBeDefined(); + expect(trigger.examples?.length).toBeGreaterThan(0); + } + }); + + it('returns custom trigger with trigger-specific eventContextSchema when filtered', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [mockCasesTrigger], + triggerType: 'cases.caseUpdated', + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + expect(result.count).toBe(1); + expect(result.triggerTypes[0].id).toBe('cases.caseUpdated'); + const schemaStr = JSON.stringify(result.triggerTypes[0].eventContextSchema); + expect(schemaStr).toContain('caseId'); + expect(schemaStr).toContain('owner'); + expect(schemaStr).toContain('spaceId'); + expect(schemaStr).toContain('timestamp'); + }); + + it('filters by built-in triggerType', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [mockCasesTrigger], + triggerType: 'scheduled', + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + expect(result.count).toBe(1); + expect(result.triggerTypes[0].id).toBe('scheduled'); + }); + + it('returns error for unknown trigger type with built-in and registered availableTypes', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [mockCasesTrigger], + triggerType: 'nonexistent', + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(true); + if (!isTriggerDefinitionsLookupError(result)) { + return; + } + + expect(result.error).toContain('not found'); + expect(result.availableTypes).toContain('manual'); + expect(result.availableTypes).toContain('cases.caseUpdated'); + }); + + it('uses documentation.examples from registered trigger definitions', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [commonWorkflowExecutionFailedTriggerDefinition], + triggerType: 'workflows.failed', + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + const trigger = result.triggerTypes[0]; + expect(trigger.examples?.length).toBeGreaterThan(0); + const examplesText = trigger.examples?.join('\n') ?? ''; + expect(examplesText).toContain('event.workflow'); + expect(examplesText).not.toContain('event.owner'); + expect(trigger.label).toBe('Workflow failed'); + expect(trigger.description).toContain('Emitted when any workflow'); + }); + + it('omits examples when registered trigger has no documentation', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [ + { + id: 'example.minimal', + eventSchema: z.object({ value: z.string() }), + }, + ], + triggerType: 'example.minimal', + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + expect(result.triggerTypes[0].examples).toBeUndefined(); + }); + + it('compacts large enums in scheduled trigger jsonSchema', () => { + const result = lookupTriggerDefinitionsForAgent({ + registeredTriggers: [mockCasesTrigger], + triggerType: 'scheduled', + }); + + expect(isTriggerDefinitionsLookupError(result)).toBe(false); + if (isTriggerDefinitionsLookupError(result)) { + return; + } + + const schemaStr = JSON.stringify(result.triggerTypes[0].jsonSchema); + expect(schemaStr).not.toContain('Pacific/Honolulu'); + expect(schemaStr).toContain('allowed values'); + }); +}); diff --git a/src/platform/plugins/shared/workflows_management/common/build_trigger_definitions_for_agent.ts b/src/platform/plugins/shared/workflows_management/common/build_trigger_definitions_for_agent.ts new file mode 100644 index 0000000000000..258f942311439 --- /dev/null +++ b/src/platform/plugins/shared/workflows_management/common/build_trigger_definitions_for_agent.ts @@ -0,0 +1,229 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { BaseTriggerDefinition } from '@kbn/workflows'; +import { + builtInTriggerDefinitions, + EventTimestampSchema, + WorkflowEventsSchema, +} from '@kbn/workflows'; +import { BaseEventSchema } from '@kbn/workflows/spec/schema/common/base_event'; +import { AlertEventSchema } from '@kbn/workflows/spec/schema/triggers/alert_trigger_schema'; +import type { ServerTriggerDefinition } from '@kbn/workflows-extensions/server'; +import { z } from '@kbn/zod/v4'; + +const LARGE_ENUM_THRESHOLD = 20; + +export interface TriggerDefinitionForAgent { + id: string; + label: string; + description: string; + jsonSchema: unknown; + eventContextSchema: unknown; + eventContextNote: string; + examples?: string[]; +} + +export const EVENT_CONTEXT_NOTE = + 'The event context is available via {{ event.* }} in Liquid templates. ' + + 'NEVER use {{ triggers.event }} or {{ trigger.event }} — the correct variable is {{ event }}.'; + +function zodToJsonSchemaSafe(schema: z.ZodType): unknown { + try { + const jsonSchema = z.toJSONSchema(schema, { target: 'draft-7', unrepresentable: 'any' }); + return compactLargeEnums(jsonSchema as Record); + } catch { + return undefined; + } +} + +function compactLargeEnums(node: unknown): unknown { + if (node === null || typeof node !== 'object') return node; + if (Array.isArray(node)) return node.map(compactLargeEnums); + + const obj = node as Record; + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + if (key === 'enum' && Array.isArray(value) && value.length > LARGE_ENUM_THRESHOLD) { + const examples = value.slice(0, 5) as string[]; + result.type = 'string'; + result.description = [ + obj.description ?? '', + `One of ${value.length} allowed values, e.g.: ${examples.join(', ')}`, + ] + .filter(Boolean) + .join('. '); + } else { + result[key] = compactLargeEnums(value); + } + } + + return result; +} + +function isZodObject(schema: z.ZodType): schema is z.ZodObject { + return schema instanceof z.ZodObject; +} + +function getCustomTriggerYamlSchema(triggerId: string): z.ZodType { + return z.object({ + type: z.literal(triggerId), + on: z + .object({ + condition: z.string().optional(), + workflowEvents: WorkflowEventsSchema.optional(), + }) + .optional(), + }); +} + +function humanizeTriggerId(triggerId: string): string { + const [namespace, event] = triggerId.split('.'); + const formatSegment = (segment: string) => + segment + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/[_-]+/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()); + if (!event) { + return formatSegment(triggerId); + } + return `${formatSegment(namespace)} - ${formatSegment(event)}`; +} + +function getEventContextSchema( + triggerId: string, + customDefsById: Map +): unknown { + if (triggerId === 'alert') { + return zodToJsonSchemaSafe(AlertEventSchema); + } + + const custom = customDefsById.get(triggerId); + if (custom && isZodObject(custom.eventSchema)) { + return zodToJsonSchemaSafe( + z.object({ + ...BaseEventSchema.shape, + ...EventTimestampSchema.shape, + ...custom.eventSchema.shape, + }) + ); + } + + return zodToJsonSchemaSafe(BaseEventSchema); +} + +function createRegisteredTriggersMap( + registeredTriggers: ServerTriggerDefinition[] +): Map { + return new Map(registeredTriggers.map((def) => [def.id, def])); +} + +function formatBuiltInTrigger( + def: BaseTriggerDefinition, + registeredTriggersById: Map +): TriggerDefinitionForAgent { + return { + id: def.id, + label: def.label, + description: def.description, + jsonSchema: zodToJsonSchemaSafe(def.schema), + eventContextSchema: getEventContextSchema(def.id, registeredTriggersById), + eventContextNote: EVENT_CONTEXT_NOTE, + examples: def.documentation.examples, + }; +} + +function formatRegisteredTrigger( + def: ServerTriggerDefinition, + registeredTriggersById: Map +): TriggerDefinitionForAgent { + const formatted: TriggerDefinitionForAgent = { + id: def.id, + label: def.title ?? humanizeTriggerId(def.id), + description: + def.description ?? + `Event-driven trigger (${def.id}). Use on.condition with KQL on event.* fields to filter when the workflow runs.`, + jsonSchema: zodToJsonSchemaSafe(getCustomTriggerYamlSchema(def.id)), + eventContextSchema: getEventContextSchema(def.id, registeredTriggersById), + eventContextNote: EVENT_CONTEXT_NOTE, + }; + + const examples = def.documentation?.examples; + if (examples && examples.length > 0) { + formatted.examples = examples; + } + + return formatted; +} + +/** + * Full trigger catalog for workflow authoring agents. + * + * - Built-ins (`manual`, `scheduled`, `alert`) come from `@kbn/workflows`. + * - Everything else comes from `api.getRegisteredTriggers()` (workflows_extensions registry). + */ +export function getAllTriggerDefinitionsForAgent( + registeredTriggers: ServerTriggerDefinition[] +): TriggerDefinitionForAgent[] { + const registeredById = createRegisteredTriggersMap(registeredTriggers); + const builtInIds = new Set(builtInTriggerDefinitions.map((def) => def.id)); + + const builtIn = builtInTriggerDefinitions.map((def) => formatBuiltInTrigger(def, registeredById)); + const pluginRegistered = registeredTriggers + .filter((def) => !builtInIds.has(def.id)) + .map((def) => formatRegisteredTrigger(def, registeredById)); + + return [...builtIn, ...pluginRegistered]; +} + +export interface TriggerDefinitionsLookupSuccess { + count: number; + triggerTypes: TriggerDefinitionForAgent[]; +} + +export interface TriggerDefinitionsLookupError { + error: string; + availableTypes: string[]; +} + +export type TriggerDefinitionsLookupResult = + | TriggerDefinitionsLookupSuccess + | TriggerDefinitionsLookupError; + +export const isTriggerDefinitionsLookupError = ( + result: TriggerDefinitionsLookupResult +): result is TriggerDefinitionsLookupError => 'error' in result; + +/** + * Resolves the agent trigger catalog, optionally filtered to a single trigger id. + */ +export function lookupTriggerDefinitionsForAgent({ + registeredTriggers, + triggerType, +}: { + registeredTriggers: ServerTriggerDefinition[]; + triggerType?: string; +}): TriggerDefinitionsLookupResult { + const allTriggerDefinitions = getAllTriggerDefinitionsForAgent(registeredTriggers); + + if (!triggerType) { + return { count: allTriggerDefinitions.length, triggerTypes: allTriggerDefinitions }; + } + + const matching = allTriggerDefinitions.filter((def) => def.id === triggerType); + if (matching.length === 0) { + return { + error: `Trigger type "${triggerType}" not found`, + availableTypes: allTriggerDefinitions.map((def) => def.id), + }; + } + + return { count: matching.length, triggerTypes: matching }; +} diff --git a/src/platform/plugins/shared/workflows_management/server/api/workflows_management_api.ts b/src/platform/plugins/shared/workflows_management/server/api/workflows_management_api.ts index 28b8138036cea..9c6ef8054524f 100644 --- a/src/platform/plugins/shared/workflows_management/server/api/workflows_management_api.ts +++ b/src/platform/plugins/shared/workflows_management/server/api/workflows_management_api.ts @@ -42,6 +42,7 @@ import type { ExecutionLogsParams, StepLogsParams, } from '@kbn/workflows-execution-engine/server/workflow_event_logger/types'; +import type { ServerTriggerDefinition } from '@kbn/workflows-extensions/server'; import { parseWorkflowYamlToJSON, stringifyWorkflowDefinition, @@ -639,6 +640,10 @@ export class WorkflowsManagementApi { return this.workflowsService.getAvailableConnectors(spaceId, request); } + public async getRegisteredTriggers(): Promise { + return this.workflowsService.getRegisteredCustomTriggerDefinitions(); + } + public async getWorkflowJsonSchema( { loose }: { loose: boolean }, spaceId: string, diff --git a/src/platform/plugins/shared/workflows_management/server/api/workflows_management_service.ts b/src/platform/plugins/shared/workflows_management/server/api/workflows_management_service.ts index 937298baadaba..c999c7a4d2589 100644 --- a/src/platform/plugins/shared/workflows_management/server/api/workflows_management_service.ts +++ b/src/platform/plugins/shared/workflows_management/server/api/workflows_management_service.ts @@ -52,7 +52,10 @@ import type { ExecutionLogsParams, StepLogsParams, } from '@kbn/workflows-execution-engine/server/workflow_event_logger/types'; -import type { WorkflowsExtensionsServerPluginStart } from '@kbn/workflows-extensions/server'; +import type { + ServerTriggerDefinition, + WorkflowsExtensionsServerPluginStart, +} from '@kbn/workflows-extensions/server'; import type { z } from '@kbn/zod/v4'; import type { StepExecutionListResult } from './lib/search_step_executions'; @@ -393,6 +396,11 @@ export class WorkflowsService { return this.validationService.getAvailableConnectors(spaceId, request); } + public async getRegisteredCustomTriggerDefinitions(): Promise { + await this.ensureInitialized(); + return this.validationService.getRegisteredCustomTriggerDefinitions(); + } + public async validateWorkflow( yaml: string, spaceId: string, diff --git a/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.test.ts b/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.test.ts index 22d6f447d495e..31d8fbd05a718 100644 --- a/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.test.ts +++ b/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.test.ts @@ -33,6 +33,27 @@ const makeDeps = ( }; describe('WorkflowValidationService', () => { + describe('getRegisteredCustomTriggerDefinitions', () => { + it('returns triggers from workflows extensions', () => { + const { deps } = makeDeps([{ id: 'cases.caseUpdated' }]); + const service = new WorkflowValidationService(deps); + + expect(service.getRegisteredCustomTriggerDefinitions()).toEqual([ + { id: 'cases.caseUpdated' }, + ]); + }); + + it('returns an empty array when workflows extensions is not available', () => { + const { deps } = makeDeps(); + const service = new WorkflowValidationService({ + ...deps, + workflowsExtensions: undefined, + }); + + expect(service.getRegisteredCustomTriggerDefinitions()).toEqual([]); + }); + }); + describe('getAvailableConnectors', () => { it('delegates to the library helper with the plumbed clients and spaceId', async () => { const { deps, actionsClient, actionsClientWithRequest } = makeDeps(); diff --git a/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.ts b/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.ts index 5701ec3135c4e..3edcc85de805f 100644 --- a/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.ts +++ b/src/platform/plugins/shared/workflows_management/server/services/workflow_validation_service.ts @@ -10,6 +10,7 @@ import type { KibanaRequest } from '@kbn/core/server'; import type { ValidateWorkflowResponseDto } from '@kbn/workflows'; import type { GetAvailableConnectorsResponse } from '@kbn/workflows/types/v1'; +import type { ServerTriggerDefinition } from '@kbn/workflows-extensions/server'; import type { z } from '@kbn/zod/v4'; import type { WorkflowValidationDeps } from './types'; @@ -32,13 +33,17 @@ export class WorkflowValidationService { }); } + getRegisteredCustomTriggerDefinitions(): ServerTriggerDefinition[] { + return this.deps.workflowsExtensions?.getAllTriggerDefinitions() ?? []; + } + async validateWorkflow( yaml: string, spaceId: string, request: KibanaRequest ): Promise { const zodSchema = await this.getWorkflowZodSchema({ loose: false }, spaceId, request); - const triggerDefinitions = this.deps.workflowsExtensions?.getAllTriggerDefinitions() ?? []; + const triggerDefinitions = this.getRegisteredCustomTriggerDefinitions(); return validateWorkflowYaml(yaml, zodSchema, { triggerDefinitions }); } @@ -48,8 +53,7 @@ export class WorkflowValidationService { request: KibanaRequest ): Promise { const { connectorTypes } = await this.getAvailableConnectors(spaceId, request); - const registeredTriggerIds = - this.deps.workflowsExtensions?.getAllTriggerDefinitions().map((t) => t.id) ?? []; + const registeredTriggerIds = this.getRegisteredCustomTriggerDefinitions().map((t) => t.id); return getWorkflowZodSchema(connectorTypes, registeredTriggerIds); } } diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/lookup.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/lookup.ts index bce504d353bfb..5006e7f955acd 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/lookup.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/lookup.ts @@ -6,12 +6,10 @@ */ import type { KibanaRequest } from '@kbn/core-http-server'; -import { builtInStepDefinitions, builtInTriggerDefinitions } from '@kbn/workflows'; -import { z } from '@kbn/zod/v4'; +import { lookupTriggerDefinitionsForAgent } from '@kbn/workflows-management-plugin/common/build_trigger_definitions_for_agent'; +import { builtInStepDefinitions } from '@kbn/workflows'; import type { WorkflowsManagementApi } from '@kbn/workflows-management-plugin/server'; import { getAllConnectors } from '@kbn/workflows-management-plugin/common/schema'; -import { AlertEventSchema } from '@kbn/workflows/spec/schema/triggers/alert_trigger_schema'; -import { BaseEventSchema } from '@kbn/workflows/spec/schema/common/base_event'; import { categorizeConnectorType, formatBuiltInStep, @@ -19,14 +17,6 @@ import { type StepDefinitionForAgent, } from './lookup_helpers'; -const zodToJsonSchemaSafe = (schema: z.ZodType): unknown => { - try { - return z.toJSONSchema(schema, { target: 'draft-7', unrepresentable: 'any' }); - } catch { - return undefined; - } -}; - export interface LookupDeps { api: WorkflowsManagementApi; spaceId: string; @@ -95,7 +85,6 @@ export const lookupStepDefinitions = async ( return { error: `No step type matching ${args.stepType ?? args.search ?? 'anything'}` }; } - // Match the existing tool: detailed only when ≤5 results. if (filtered.length > 5) { return { count: filtered.length, @@ -105,30 +94,11 @@ export const lookupStepDefinitions = async ( return { count: filtered.length, stepTypes: filtered }; }; -export const lookupTriggerDefinitions = async (args: { - triggerType?: string; -}): Promise => { - let defs = builtInTriggerDefinitions.map((def) => ({ - id: def.id, - label: def.label, - description: def.description, - jsonSchema: zodToJsonSchemaSafe(def.schema), - eventContextSchema: - def.id === 'alert' - ? zodToJsonSchemaSafe(AlertEventSchema) - : zodToJsonSchemaSafe(BaseEventSchema), - examples: def.documentation?.examples, - })); - - if (args.triggerType) { - defs = defs.filter((d) => d.id === args.triggerType); - } - - if (defs.length === 0) { - return { - error: `Trigger type "${args.triggerType}" not found`, - availableTypes: builtInTriggerDefinitions.map((d) => d.id), - }; - } - return { count: defs.length, triggerTypes: defs }; -}; +export const lookupTriggerDefinitions = async ( + args: { triggerType?: string }, + { api }: LookupDeps +): Promise => + lookupTriggerDefinitionsForAgent({ + registeredTriggers: await api.getRegisteredTriggers(), + triggerType: args.triggerType, + }); diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.test.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.test.ts index 9f42ecb8f0cbf..0ac56db5d6596 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.test.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.test.ts @@ -190,7 +190,7 @@ describe('dispatchToolCall', () => { baseDeps ); - expect(mockLookupTriggerDefinitions).toHaveBeenCalledWith({ triggerType: 'alert' }); + expect(mockLookupTriggerDefinitions).toHaveBeenCalledWith({ triggerType: 'alert' }, baseDeps); expect(result.yaml).toBeUndefined(); expect(result.message.success).toBe(true); expect(result.message.data).toEqual({ count: 1, triggerTypes: ['alert'] }); diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.ts index cac2d76bd7f3b..37540e3acd611 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-workflow-gen/generate_workflow/tools/tools.ts @@ -180,8 +180,8 @@ const getTriggerDefinitionsTool = defineTool({ triggerType: z.string().optional(), }), mutatesYaml: false, - execute: async (args) => ({ - message: { success: true, data: await lookupTriggerDefinitions(args) }, + execute: async (args, { deps }) => ({ + message: { success: true, data: await lookupTriggerDefinitions(args, deps) }, }), }); diff --git a/x-pack/platform/plugins/shared/agent_builder_workflows/server/plugin.ts b/x-pack/platform/plugins/shared/agent_builder_workflows/server/plugin.ts index 4fa749175f6b9..614d3f1ed9fb5 100644 --- a/x-pack/platform/plugins/shared/agent_builder_workflows/server/plugin.ts +++ b/x-pack/platform/plugins/shared/agent_builder_workflows/server/plugin.ts @@ -63,7 +63,7 @@ export class AgentBuilderWorkflowsPlugin // Workflow tools registerValidateWorkflowTool(agentBuilder, api); registerGetStepDefinitionsTool(agentBuilder, api); - registerGetTriggerDefinitionsTool(agentBuilder); + registerGetTriggerDefinitionsTool(agentBuilder, api); registerGetConnectorsTool(agentBuilder, api); registerGetExamplesTool(agentBuilder); registerWorkflowExecuteStepTool(agentBuilder, api); diff --git a/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.test.ts b/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.test.ts index 29ec15a9e658c..1a347b675d21c 100644 --- a/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.test.ts +++ b/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.test.ts @@ -7,16 +7,35 @@ import type { BuiltinToolDefinition } from '@kbn/agent-builder-server'; import type { ToolHandlerStandardReturn } from '@kbn/agent-builder-server/tools'; -import { builtInTriggerDefinitions } from '@kbn/workflows'; +import type { WorkflowsServerPluginSetup } from '@kbn/workflows-management-plugin/server'; +import { z } from '@kbn/zod/v4'; import { registerGetTriggerDefinitionsTool } from './get_trigger_definitions_tool'; +type GetRegisteredTriggersApi = Pick< + WorkflowsServerPluginSetup['management'], + 'getRegisteredTriggers' +>; + const invokeHandler = async (tool: BuiltinToolDefinition, input: unknown, context: unknown) => (await tool.handler(input as never, context as never)) as ToolHandlerStandardReturn; +const mockCasesTrigger = { + id: 'cases.caseUpdated', + eventSchema: z.object({ + caseId: z.string(), + owner: z.string(), + }), +}; + describe('registerGetTriggerDefinitionsTool', () => { let registeredTool: BuiltinToolDefinition; + let mockApi: jest.Mocked; beforeEach(() => { + mockApi = { + getRegisteredTriggers: jest.fn().mockResolvedValue([mockCasesTrigger]), + }; + const agentBuilder = { tools: { register: jest.fn((tool: BuiltinToolDefinition) => { @@ -24,60 +43,22 @@ describe('registerGetTriggerDefinitionsTool', () => { }), }, } as any; - registerGetTriggerDefinitionsTool(agentBuilder); + registerGetTriggerDefinitionsTool(agentBuilder, mockApi); }); it('registers with correct id', () => { expect(registeredTool.id).toBe('platform.workflows.get_trigger_definitions'); }); - it('returns all trigger types without filter', async () => { - const result = await invokeHandler(registeredTool, {}, {}); - const data = result.results[0].data as any; - expect(data.count).toBe(builtInTriggerDefinitions.length); - expect(data.triggerTypes.length).toBe(builtInTriggerDefinitions.length); - }); - - it('returns jsonSchema and examples for each trigger', async () => { - const result = await invokeHandler(registeredTool, {}, {}); - const data = result.results[0].data as any; - for (const trigger of data.triggerTypes) { - expect(trigger).toHaveProperty('id'); - expect(trigger).toHaveProperty('label'); - expect(trigger).toHaveProperty('description'); - expect(trigger).toHaveProperty('jsonSchema'); - expect(trigger).toHaveProperty('examples'); - expect(trigger.examples.length).toBeGreaterThan(0); - } - }); - - it('filters by triggerType', async () => { - const result = await invokeHandler(registeredTool, { triggerType: 'scheduled' }, {}); - const data = result.results[0].data as any; - expect(data.count).toBe(1); - expect(data.triggerTypes[0].id).toBe('scheduled'); - }); - - it('returns error for unknown trigger type', async () => { - const result = await invokeHandler(registeredTool, { triggerType: 'nonexistent' }, {}); - const data = result.results[0].data as any; - expect(data.error).toContain('not found'); - expect(data.availableTypes).toContain('manual'); - }); - - it('compacts large enums like timezone names in the schema', async () => { - const result = await invokeHandler(registeredTool, { triggerType: 'scheduled' }, {}); - const data = result.results[0].data as any; - const schemaStr = JSON.stringify(data.triggerTypes[0].jsonSchema); - expect(schemaStr).not.toContain('Pacific/Honolulu'); - expect(schemaStr).toContain('allowed values'); - }); + it('wraps lookup result in agent builder tool response shape', async () => { + const result = await invokeHandler(registeredTool, { triggerType: 'manual' }, {}); - it('returns results in expected shape', async () => { - const result = await invokeHandler(registeredTool, {}, {}); - expect(result).toHaveProperty('results'); + expect(mockApi.getRegisteredTriggers).toHaveBeenCalled(); expect(result.results).toHaveLength(1); expect(result.results[0].type).toBe('other'); - expect(result.results[0]).toHaveProperty('data'); + expect(result.results[0].data).toEqual({ + count: 1, + triggerTypes: [expect.objectContaining({ id: 'manual' })], + }); }); }); diff --git a/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.ts b/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.ts index fa2b43f62696b..b6e79b34dcc20 100644 --- a/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.ts +++ b/x-pack/platform/plugins/shared/agent_builder_workflows/server/tools/get_trigger_definitions_tool.ts @@ -6,68 +6,21 @@ */ import { ToolType } from '@kbn/agent-builder-common'; -import { builtInTriggerDefinitions } from '@kbn/workflows'; -import { BaseEventSchema } from '@kbn/workflows/spec/schema/common/base_event'; -import { AlertEventSchema } from '@kbn/workflows/spec/schema/triggers/alert_trigger_schema'; +import type { WorkflowsServerPluginSetup } from '@kbn/workflows-management-plugin/server'; +import { lookupTriggerDefinitionsForAgent } from '@kbn/workflows-management-plugin/common/build_trigger_definitions_for_agent'; import { z } from '@kbn/zod/v4'; import type { AgentBuilderPluginSetup } from '@kbn/agent-builder-server'; import { workflowTools } from '../../common/constants'; -const LARGE_ENUM_THRESHOLD = 20; +type GetRegisteredTriggersApi = Pick< + WorkflowsServerPluginSetup['management'], + 'getRegisteredTriggers' +>; -function zodToJsonSchemaSafe(schema: z.ZodType): unknown { - try { - const jsonSchema = z.toJSONSchema(schema, { target: 'draft-7', unrepresentable: 'any' }); - return compactLargeEnums(jsonSchema as Record); - } catch { - return undefined; - } -} - -/** - * Recursively walk a JSON Schema and replace enum arrays larger than - * {@link LARGE_ENUM_THRESHOLD} with a compact description + a few examples. - * This avoids sending 600+ timezone names (or similar) to the LLM. - */ -function compactLargeEnums(node: unknown): unknown { - if (node === null || typeof node !== 'object') return node; - if (Array.isArray(node)) return node.map(compactLargeEnums); - - const obj = node as Record; - const result: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - if (key === 'enum' && Array.isArray(value) && value.length > LARGE_ENUM_THRESHOLD) { - const examples = value.slice(0, 5) as string[]; - result.type = 'string'; - result.description = [ - obj.description ?? '', - `One of ${value.length} allowed values, e.g.: ${examples.join(', ')}`, - ] - .filter(Boolean) - .join('. '); - } else { - result[key] = compactLargeEnums(value); - } - } - - return result; -} - -/** - * Returns the JSON Schema for `{{ event.* }}` variables available at runtime for a given trigger type. - * Alert triggers get the full alert event context (alerts array, rule, params); - * other built-in triggers only get `BaseEventSchema` (spaceId). - */ -function getEventContextSchema(triggerTypeId: string): unknown { - // TODO: support custom trigger event schemas - if (triggerTypeId === 'alert') { - return zodToJsonSchemaSafe(AlertEventSchema); - } - return zodToJsonSchemaSafe(BaseEventSchema); -} - -export function registerGetTriggerDefinitionsTool(agentBuilder: AgentBuilderPluginSetup): void { +export function registerGetTriggerDefinitionsTool( + agentBuilder: AgentBuilderPluginSetup, + api: GetRegisteredTriggersApi +): void { agentBuilder.tools.register({ id: workflowTools.getTriggerDefinitions, type: ToolType.builtin, @@ -76,57 +29,27 @@ export function registerGetTriggerDefinitionsTool(agentBuilder: AgentBuilderPlug **When to use:** To learn how to configure the \`triggers\` section of a workflow, or to understand what \`{{ event.* }}\` variables are available at runtime for a given trigger type. **When NOT to use:** For step definitions (use get_step_definitions) or connector instances (use get_connectors). -Returns built-in trigger types (manual, scheduled, alert) including the event context schema that describes what \`{{ event.* }}\` contains at runtime.`, +Returns built-in trigger types (manual, scheduled, alert) plus event-driven triggers (e.g. cases.caseUpdated) including the event context schema that describes what \`{{ event.* }}\` contains at runtime.`, schema: z.object({ triggerType: z .string() .optional() - .describe('Filter by exact trigger type (e.g., "manual", "scheduled", "alert")'), + .describe( + 'Filter by exact trigger type (e.g., "manual", "scheduled", "alert", "workflows.failed", "cases.caseUpdated")' + ), }), tags: ['workflows', 'yaml', 'triggers'], experimental: true, - handler: async ({ triggerType }) => { - let definitions = builtInTriggerDefinitions.map((def) => ({ - id: def.id, - label: def.label, - description: def.description, - jsonSchema: zodToJsonSchemaSafe(def.schema), - eventContextSchema: getEventContextSchema(def.id), - eventContextNote: - 'The event context is available via {{ event.* }} in Liquid templates. ' + - 'NEVER use {{ triggers.event }} or {{ trigger.event }} — the correct variable is {{ event }}.', - examples: def.documentation.examples, - })); - - if (triggerType) { - definitions = definitions.filter((def) => def.id === triggerType); - } - - if (definitions.length === 0 && triggerType) { - return { - results: [ - { - type: 'other' as const, - data: { - error: `Trigger type "${triggerType}" not found`, - availableTypes: builtInTriggerDefinitions.map((d) => d.id), - }, - }, - ], - }; - } - - return { - results: [ - { - type: 'other' as const, - data: { - count: definitions.length, - triggerTypes: definitions, - }, - }, - ], - }; - }, + handler: async ({ triggerType }) => ({ + results: [ + { + type: 'other' as const, + data: lookupTriggerDefinitionsForAgent({ + registeredTriggers: await api.getRegisteredTriggers(), + triggerType, + }), + }, + ], + }), }); } diff --git a/x-pack/platform/plugins/shared/cases/common/workflows/triggers/index.ts b/x-pack/platform/plugins/shared/cases/common/workflows/triggers/index.ts index 4d79a3144e72e..8ac11a365e106 100644 --- a/x-pack/platform/plugins/shared/cases/common/workflows/triggers/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/workflows/triggers/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { z } from '@kbn/zod/v4'; import type { CommonTriggerDefinition } from '@kbn/workflows-extensions/common'; import { Owner as OwnerSchema } from '../../bundled-types.gen'; @@ -29,6 +30,32 @@ const baseCaseEventSchema = z.object({ export const caseCreatedTriggerCommonDefinition: CommonTriggerDefinition = { id: CaseCreatedTriggerId, eventSchema: baseCaseEventSchema, + title: i18n.translate('xpack.cases.workflowTriggers.caseCreated.title', { + defaultMessage: 'Cases - Case created', + }), + description: i18n.translate('xpack.cases.workflowTriggers.caseCreated.description', { + defaultMessage: 'Emitted when a case is created.', + }), + documentation: { + details: i18n.translate('xpack.cases.workflowTriggers.caseCreated.documentation.details', { + defaultMessage: + 'Emitted after a case is created. The payload includes event.caseId and event.owner, which you can use in trigger conditions.', + }), + examples: [ + i18n.translate('xpack.cases.workflowTriggers.caseCreated.documentation.example', { + defaultMessage: `## Run for Security cases only +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.owner: "securitySolution"' +\`\`\``, + values: { + triggerId: CaseCreatedTriggerId, + }, + }), + ], + }, }; export const CaseUpdatedTriggerId = 'cases.caseUpdated' as const; @@ -43,6 +70,32 @@ const caseUpdatedEventSchema = baseCaseEventSchema.extend({ export const caseUpdatedTriggerCommonDefinition: CommonTriggerDefinition = { id: CaseUpdatedTriggerId, eventSchema: caseUpdatedEventSchema, + title: i18n.translate('xpack.cases.workflowTriggers.caseUpdated.title', { + defaultMessage: 'Cases - Case updated', + }), + description: i18n.translate('xpack.cases.workflowTriggers.caseUpdated.description', { + defaultMessage: 'Emitted when a case is updated.', + }), + documentation: { + details: i18n.translate('xpack.cases.workflowTriggers.caseUpdated.documentation.details', { + defaultMessage: + 'Emitted after case updates. Use event.updatedFields to filter by which fields changed, event.caseId to match a specific case, and event.owner to scope by case owner.', + }), + examples: [ + i18n.translate('xpack.cases.workflowTriggers.caseUpdated.documentation.example', { + defaultMessage: `## Run when Security case status changes +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.owner: "securitySolution" and event.updatedFields: "status"' +\`\`\``, + values: { + triggerId: CaseUpdatedTriggerId, + }, + }), + ], + }, }; export const CaseStatusUpdatedTriggerId = 'cases.caseStatusUpdated' as const; @@ -59,6 +112,35 @@ const caseStatusUpdatedEventSchema = baseCaseEventSchema.extend({ export const caseStatusUpdatedTriggerCommonDefinition: CommonTriggerDefinition = { id: CaseStatusUpdatedTriggerId, eventSchema: caseStatusUpdatedEventSchema, + title: i18n.translate('xpack.cases.workflowTriggers.caseStatusUpdated.title', { + defaultMessage: 'Cases - Case status updated', + }), + description: i18n.translate('xpack.cases.workflowTriggers.caseStatusUpdated.description', { + defaultMessage: 'Emitted when a case status is updated.', + }), + documentation: { + details: i18n.translate( + 'xpack.cases.workflowTriggers.caseStatusUpdated.documentation.details', + { + defaultMessage: + 'Emitted after case status updates. Includes the current and previous status.', + } + ), + examples: [ + i18n.translate('xpack.cases.workflowTriggers.caseStatusUpdated.documentation.example', { + defaultMessage: `## Run when Security case is closed +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.owner: "securitySolution" and event.status: "closed"' +\`\`\``, + values: { + triggerId: CaseStatusUpdatedTriggerId, + }, + }), + ], + }, }; export const AttachmentsAddedTriggerId = 'cases.attachmentsAdded' as const; @@ -75,6 +157,50 @@ const attachmentsAddedEventSchema = baseCaseEventSchema.extend({ export const attachmentsAddedTriggerCommonDefinition: CommonTriggerDefinition = { id: AttachmentsAddedTriggerId, eventSchema: attachmentsAddedEventSchema, + title: i18n.translate('xpack.cases.workflowTriggers.attachmentsAdded.title', { + defaultMessage: 'Cases - Attachments added', + }), + description: i18n.translate('xpack.cases.workflowTriggers.attachmentsAdded.description', { + defaultMessage: 'Emitted when one or more attachments of the same type are added to a case.', + }), + documentation: { + details: i18n.translate('xpack.cases.workflowTriggers.attachmentsAdded.documentation.details', { + defaultMessage: + 'Emitted after attachments are added to a case, once per attachment type involved. The payload includes event.caseId, event.owner, event.attachmentIds (all IDs added in that operation for this type), and event.attachmentType (e.g. "comment", "alert"). Use KQL on event.* for trigger conditions.', + }), + examples: [ + i18n.translate( + 'xpack.cases.workflowTriggers.attachmentsAdded.documentation.exampleCaseFilter', + { + defaultMessage: `## Run only for Security cases +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.owner: "securitySolution"' +\`\`\``, + values: { + triggerId: AttachmentsAddedTriggerId, + }, + } + ), + i18n.translate( + 'xpack.cases.workflowTriggers.attachmentsAdded.documentation.exampleTypeFilter', + { + defaultMessage: `## Run only when a comment is added +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.attachmentType: "comment"' +\`\`\``, + values: { + triggerId: AttachmentsAddedTriggerId, + }, + } + ), + ], + }, }; export const CommentsAddedTriggerId = 'cases.commentsAdded' as const; @@ -88,4 +214,30 @@ const CommentsAddedEventSchema = baseCaseEventSchema.extend({ export const commentsAddedTriggerCommonDefinition: CommonTriggerDefinition = { id: CommentsAddedTriggerId, eventSchema: CommentsAddedEventSchema, + title: i18n.translate('xpack.cases.workflowTriggers.commentsAdded.title', { + defaultMessage: 'Cases - Comments added', + }), + description: i18n.translate('xpack.cases.workflowTriggers.commentsAdded.description', { + defaultMessage: 'Emitted when one or more comments are added to a case.', + }), + documentation: { + details: i18n.translate('xpack.cases.workflowTriggers.commentsAdded.documentation.details', { + defaultMessage: + 'Emitted after comments are added to a case. The payload includes event.caseId, event.owner, event.commentIds. Use KQL on event.* for trigger conditions.', + }), + examples: [ + i18n.translate('xpack.cases.workflowTriggers.commentsAdded.documentation.exampleCaseFilter', { + defaultMessage: `## Run only for Security cases +\`\`\`yaml +triggers: + - type: {triggerId} + on: + condition: 'event.owner: "securitySolution"' +\`\`\``, + values: { + triggerId: CommentsAddedTriggerId, + }, + }), + ], + }, }; diff --git a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/attachments_added.ts b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/attachments_added.ts index 166b09b7721e8..05355c2860e92 100644 --- a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/attachments_added.ts +++ b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/attachments_added.ts @@ -6,12 +6,8 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; -import { - AttachmentsAddedTriggerId, - attachmentsAddedTriggerCommonDefinition, -} from '../../../common/workflows/triggers'; +import { attachmentsAddedTriggerCommonDefinition } from '../../../common/workflows/triggers'; export const attachmentsAddedTriggerPublicDefinition: PublicTriggerDefinition = { ...attachmentsAddedTriggerCommonDefinition, @@ -20,48 +16,4 @@ export const attachmentsAddedTriggerPublicDefinition: PublicTriggerDefinition = default: icon, })) ), - title: i18n.translate('xpack.cases.workflowTriggers.attachmentsAdded.title', { - defaultMessage: 'Cases - Attachments added', - }), - description: i18n.translate('xpack.cases.workflowTriggers.attachmentsAdded.description', { - defaultMessage: 'Emitted when one or more attachments of the same type are added to a case.', - }), - documentation: { - details: i18n.translate('xpack.cases.workflowTriggers.attachmentsAdded.documentation.details', { - defaultMessage: - 'Emitted after attachments are added to a case, once per attachment type involved. The payload includes event.caseId, event.owner, event.attachmentIds (all IDs added in that operation for this type), and event.attachmentType (e.g. "comment", "alert"). Use KQL on event.* for trigger conditions.', - }), - examples: [ - i18n.translate( - 'xpack.cases.workflowTriggers.attachmentsAdded.documentation.exampleCaseFilter', - { - defaultMessage: `## Run only for Security cases -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.owner: "securitySolution"' -\`\`\``, - values: { - triggerId: AttachmentsAddedTriggerId, - }, - } - ), - i18n.translate( - 'xpack.cases.workflowTriggers.attachmentsAdded.documentation.exampleTypeFilter', - { - defaultMessage: `## Run only when a comment is added -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.attachmentType: "comment"' -\`\`\``, - values: { - triggerId: AttachmentsAddedTriggerId, - }, - } - ), - ], - }, }; diff --git a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_created.ts b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_created.ts index ec5a77eb95213..d1cd883152f26 100644 --- a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_created.ts +++ b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_created.ts @@ -6,12 +6,8 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; -import { - CaseCreatedTriggerId, - caseCreatedTriggerCommonDefinition, -} from '../../../common/workflows/triggers'; +import { caseCreatedTriggerCommonDefinition } from '../../../common/workflows/triggers'; export const caseCreatedTriggerPublicDefinition: PublicTriggerDefinition = { ...caseCreatedTriggerCommonDefinition, @@ -20,30 +16,4 @@ export const caseCreatedTriggerPublicDefinition: PublicTriggerDefinition = { default: icon, })) ), - title: i18n.translate('xpack.cases.workflowTriggers.caseCreated.title', { - defaultMessage: 'Cases - Case created', - }), - description: i18n.translate('xpack.cases.workflowTriggers.caseCreated.description', { - defaultMessage: 'Emitted when a case is created.', - }), - documentation: { - details: i18n.translate('xpack.cases.workflowTriggers.caseCreated.documentation.details', { - defaultMessage: - 'Emitted after a case is created. The payload includes event.caseId and event.owner, which you can use in trigger conditions.', - }), - examples: [ - i18n.translate('xpack.cases.workflowTriggers.caseCreated.documentation.example', { - defaultMessage: `## Run for Security cases only -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.owner: "securitySolution"' -\`\`\``, - values: { - triggerId: CaseCreatedTriggerId, - }, - }), - ], - }, }; diff --git a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_status_updated.ts b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_status_updated.ts index c307a959cc104..0701d2afc6470 100644 --- a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_status_updated.ts +++ b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_status_updated.ts @@ -6,12 +6,8 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; -import { - CaseStatusUpdatedTriggerId, - caseStatusUpdatedTriggerCommonDefinition, -} from '../../../common/workflows/triggers'; +import { caseStatusUpdatedTriggerCommonDefinition } from '../../../common/workflows/triggers'; export const caseStatusUpdatedTriggerPublicDefinition: PublicTriggerDefinition = { ...caseStatusUpdatedTriggerCommonDefinition, @@ -20,33 +16,4 @@ export const caseStatusUpdatedTriggerPublicDefinition: PublicTriggerDefinition = default: icon, })) ), - title: i18n.translate('xpack.cases.workflowTriggers.caseStatusUpdated.title', { - defaultMessage: 'Cases - Case status updated', - }), - description: i18n.translate('xpack.cases.workflowTriggers.caseStatusUpdated.description', { - defaultMessage: 'Emitted when a case status is updated.', - }), - documentation: { - details: i18n.translate( - 'xpack.cases.workflowTriggers.caseStatusUpdated.documentation.details', - { - defaultMessage: - 'Emitted after case status updates. Includes the current and previous status.', - } - ), - examples: [ - i18n.translate('xpack.cases.workflowTriggers.caseStatusUpdated.documentation.example', { - defaultMessage: `## Run when Security case is closed -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.owner: "securitySolution" and event.status: "closed"' -\`\`\``, - values: { - triggerId: CaseStatusUpdatedTriggerId, - }, - }), - ], - }, }; diff --git a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_updated.ts b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_updated.ts index c3d5ab29c824c..c6a7e15088225 100644 --- a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_updated.ts +++ b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/case_updated.ts @@ -6,12 +6,8 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; -import { - CaseUpdatedTriggerId, - caseUpdatedTriggerCommonDefinition, -} from '../../../common/workflows/triggers'; +import { caseUpdatedTriggerCommonDefinition } from '../../../common/workflows/triggers'; export const caseUpdatedTriggerPublicDefinition: PublicTriggerDefinition = { ...caseUpdatedTriggerCommonDefinition, @@ -20,30 +16,4 @@ export const caseUpdatedTriggerPublicDefinition: PublicTriggerDefinition = { default: icon, })) ), - title: i18n.translate('xpack.cases.workflowTriggers.caseUpdated.title', { - defaultMessage: 'Cases - Case updated', - }), - description: i18n.translate('xpack.cases.workflowTriggers.caseUpdated.description', { - defaultMessage: 'Emitted when a case is updated.', - }), - documentation: { - details: i18n.translate('xpack.cases.workflowTriggers.caseUpdated.documentation.details', { - defaultMessage: - 'Emitted after case updates. Use event.updatedFields to filter by which fields changed, event.caseId to match a specific case, and event.owner to scope by case owner.', - }), - examples: [ - i18n.translate('xpack.cases.workflowTriggers.caseUpdated.documentation.example', { - defaultMessage: `## Run when Security case status changes -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.owner: "securitySolution" and event.updatedFields: "status"' -\`\`\``, - values: { - triggerId: CaseUpdatedTriggerId, - }, - }), - ], - }, }; diff --git a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/comments_added.ts b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/comments_added.ts index 448703bbcb463..f9cda79a5a37a 100644 --- a/x-pack/platform/plugins/shared/cases/public/workflows/triggers/comments_added.ts +++ b/x-pack/platform/plugins/shared/cases/public/workflows/triggers/comments_added.ts @@ -6,12 +6,8 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { PublicTriggerDefinition } from '@kbn/workflows-extensions/public'; -import { - CommentsAddedTriggerId, - commentsAddedTriggerCommonDefinition, -} from '../../../common/workflows/triggers'; +import { commentsAddedTriggerCommonDefinition } from '../../../common/workflows/triggers'; export const commentsAddedTriggerPublicDefinition: PublicTriggerDefinition = { ...commentsAddedTriggerCommonDefinition, @@ -20,30 +16,4 @@ export const commentsAddedTriggerPublicDefinition: PublicTriggerDefinition = { default: icon, })) ), - title: i18n.translate('xpack.cases.workflowTriggers.commentsAdded.title', { - defaultMessage: 'Cases - Comments added', - }), - description: i18n.translate('xpack.cases.workflowTriggers.commentsAdded.description', { - defaultMessage: 'Emitted when one or more comments are added to a case.', - }), - documentation: { - details: i18n.translate('xpack.cases.workflowTriggers.commentsAdded.documentation.details', { - defaultMessage: - 'Emitted after comments are added to a case. The payload includes event.caseId, event.owner, event.commentIds. Use KQL on event.* for trigger conditions.', - }), - examples: [ - i18n.translate('xpack.cases.workflowTriggers.commentsAdded.documentation.exampleCaseFilter', { - defaultMessage: `## Run only for Security cases -\`\`\`yaml -triggers: - - type: {triggerId} - on: - condition: 'event.owner: "securitySolution"' -\`\`\``, - values: { - triggerId: CommentsAddedTriggerId, - }, - }), - ], - }, };