Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 { WorkflowDetailDto } from '@kbn/workflows';
import { validateWorkflowForExecution } from './validate_workflow_for_execution';

const createMockWorkflow = (overrides: Partial<WorkflowDetailDto> = {}): WorkflowDetailDto => ({
id: 'test-workflow-id',
name: 'Test Workflow',
description: 'A test workflow',
enabled: true,
createdAt: '2026-01-01T00:00:00Z',
createdBy: 'user1',
lastUpdatedAt: '2026-01-01T00:00:00Z',
lastUpdatedBy: 'user1',
definition: {
triggers: [],
steps: [],
} as unknown as WorkflowDetailDto['definition'],
yaml: 'triggers: []\nsteps: []',
valid: true,
...overrides,
});

describe('validateWorkflowForExecution', () => {
it('should not throw for a valid, enabled workflow', () => {
const workflow = createMockWorkflow();

expect(() => validateWorkflowForExecution(workflow, 'test-workflow-id')).not.toThrow();
});

it('should throw when workflow is null (not found)', () => {
expect(() => validateWorkflowForExecution(null, 'missing-id')).toThrow(
'Workflow not found: missing-id'
);
});

it('should throw when workflow definition is missing', () => {
const workflow = createMockWorkflow({ definition: null });

expect(() => validateWorkflowForExecution(workflow, 'test-workflow-id')).toThrow(
'Workflow definition not found: test-workflow-id'
);
});

it('should throw when workflow is not valid', () => {
const workflow = createMockWorkflow({ valid: false });

expect(() => validateWorkflowForExecution(workflow, 'test-workflow-id')).toThrow(
'Workflow is not valid: test-workflow-id'
);
});

it('should throw when workflow is disabled', () => {
const workflow = createMockWorkflow({ enabled: false });

expect(() => validateWorkflowForExecution(workflow, 'test-workflow-id')).toThrow(
'Workflow is disabled: test-workflow-id. Enable the workflow to run it.'
);
});

it('should check conditions in order: not found > no definition > not valid > disabled', () => {
// A null workflow should throw "not found", not any other error
expect(() => validateWorkflowForExecution(null, 'wf-1')).toThrow('Workflow not found: wf-1');

// A workflow with no definition should throw "definition not found" even if also invalid and disabled
const noDefAndInvalidAndDisabled = createMockWorkflow({
definition: null,
valid: false,
enabled: false,
});
expect(() => validateWorkflowForExecution(noDefAndInvalidAndDisabled, 'wf-2')).toThrow(
'Workflow definition not found: wf-2'
);

// An invalid workflow should throw "not valid" even if also disabled
const invalidAndDisabled = createMockWorkflow({ valid: false, enabled: false });
expect(() => validateWorkflowForExecution(invalidAndDisabled, 'wf-3')).toThrow(
'Workflow is not valid: wf-3'
);

// A disabled but otherwise valid workflow should throw "disabled"
const disabledOnly = createMockWorkflow({ enabled: false });
expect(() => validateWorkflowForExecution(disabledOnly, 'wf-4')).toThrow(
'Workflow is disabled: wf-4. Enable the workflow to run it.'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { WorkflowDetailDto } from '@kbn/workflows';

/**
* Validates that a workflow is in a runnable state before execution.
* Throws a descriptive error if any validation check fails.
*
* Uses TypeScript assertion signature so that after a successful call,
* the compiler narrows `workflow` to a non-null `WorkflowDetailDto`
* with a guaranteed `definition`.
*/
export function validateWorkflowForExecution(
workflow: WorkflowDetailDto | null,
workflowId: string
): asserts workflow is WorkflowDetailDto & {
definition: NonNullable<WorkflowDetailDto['definition']>;
} {
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}

if (!workflow.definition) {
throw new Error(`Workflow definition not found: ${workflowId}`);
}

if (!workflow.valid) {
throw new Error(`Workflow is not valid: ${workflowId}`);
}

if (!workflow.enabled) {
throw new Error(`Workflow is disabled: ${workflowId}. Enable the workflow to run it.`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getWorkflowsConnectorAdapter,
getConnectorType as getWorkflowsConnectorType,
} from './connectors/workflows';
import { validateWorkflowForExecution } from './connectors/workflows/validate_workflow_for_execution';
import { WorkflowsManagementFeatureConfig } from './features';
import { WorkflowTaskScheduler } from './tasks/workflow_task_scheduler';
import type {
Expand Down Expand Up @@ -77,19 +78,9 @@ export class WorkflowsPlugin
throw new Error('Workflows management API not initialized');
}

// Get the workflow first
// Get the workflow and validate it is in a runnable state
const workflow = await this.api.getWorkflow(workflowId, spaceId);
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}

if (!workflow.definition) {
throw new Error(`Workflow definition not found: ${workflowId}`);
}

if (!workflow.valid) {
throw new Error(`Workflow is not valid: ${workflowId}`);
}
validateWorkflowForExecution(workflow, workflowId);

const workflowToRun: WorkflowExecutionEngineModel = {
id: workflow.id,
Expand All @@ -116,18 +107,9 @@ export class WorkflowsPlugin
throw new Error('Workflows management API not initialized');
}

// Get the workflow and validate it is in a runnable state
const workflow = await this.api.getWorkflow(workflowId, spaceId);
if (!workflow) {
throw new Error(`Workflow not found: ${workflowId}`);
}

if (!workflow.definition) {
throw new Error(`Workflow definition not found: ${workflowId}`);
}

if (!workflow.valid) {
throw new Error(`Workflow is not valid: ${workflowId}`);
}
validateWorkflowForExecution(workflow, workflowId);

const workflowToSchedule: WorkflowExecutionEngineModel = {
id: workflow.id,
Expand Down
Loading