diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/plugin.ts b/src/platform/plugins/shared/workflows_execution_engine/server/plugin.ts index f7bd9344f2d37..a41f98498e3cd 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/plugin.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/plugin.ts @@ -1011,6 +1011,9 @@ export class WorkflowsExecutionEnginePlugin spaceId, fakeRequest: request, }); + + // Same idea as cancel: nudge TM so the resume task runs as soon as possible + await workflowTaskManager.forceRunIdleTasks(executionId); }; const workflowEventLoggerService = new WorkflowEventLoggerService( diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.test.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.test.tsx index f24709602845e..0122f475e1db6 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.test.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.test.tsx @@ -261,5 +261,28 @@ describe('ResumeExecutionButton', () => { expect(screen.getByTestId('provideActionButton')).not.toBeDisabled(); }); }); + + it('re-enables the button when waitingStepExecutionId changes after a successful submit', async () => { + const { rerender } = render( + + + + ); + fireEvent.click(screen.getByTestId('provideActionButton')); + await waitFor(() => expect(capturedOnSubmit).toBeDefined()); + act(() => { + capturedOnSubmit!({ stepInputs: { approved: true } }); + }); + await waitFor(() => { + expect(screen.getByTestId('provideActionButton')).toBeDisabled(); + }); + + rerender( + + + + ); + expect(screen.getByTestId('provideActionButton')).not.toBeDisabled(); + }); }); }); diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.tsx index 86b4f2a07d765..0a4c457948628 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/resume_execution_button.tsx @@ -28,6 +28,8 @@ interface ResumeExecutionButtonProps { resumeSchema?: JsonModelSchemaType; /** When true, opens the input modal immediately on mount */ autoOpen?: boolean; + /** Step execution document id for the active waitForInput pause; when it changes, re-enable after a prior submit */ + waitingStepExecutionId?: string; } export const ResumeExecutionButton: React.FC = ({ @@ -35,6 +37,7 @@ export const ResumeExecutionButton: React.FC = ({ resumeMessage, resumeSchema, autoOpen = false, + waitingStepExecutionId, }) => { const { notifications } = useKibana().services; const workflowsApi = useWorkflowsApi(); @@ -49,6 +52,10 @@ export const ResumeExecutionButton: React.FC = ({ if (autoOpen) setIsModalOpen(true); }, [autoOpen]); + useEffect(() => { + setIsSubmitted(false); + }, [waitingStepExecutionId]); + const contextOverride = useMemo(() => { if (!resumeSchema) return undefined; try { diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.test.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.test.tsx index eb9cfa1487e54..ffa03a82c6cc1 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.test.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.test.tsx @@ -490,7 +490,7 @@ describe('WorkflowExecutionDetail - resume input resolution', () => { } as const; // Polling returns lightweight steps without input (includeInput: false). - // Resume copy is loaded via useStepExecution(pausedStepId) — not from workflowDefinition. + // Resume copy is loaded via useStepExecution(waitingStepExecutionId) — not from workflowDefinition. // We still embed nested `if` / `foreach` shapes below so regressions in flat stepExecutions // detection do not go unnoticed when YAML nesting matches real workflows. const makeWaitingExecution = (options?: { diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.tsx index edc98925cd01a..50b40f5b63710 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_detail.tsx @@ -109,8 +109,8 @@ export const WorkflowExecutionDetail: React.FC = R const { childExecutions, isLoading: isLoadingChildExecutions } = useChildWorkflowExecutions(workflowExecution); - // Find the lightweight paused step (polling uses includeInput: false) - const pausedStepId = useMemo(() => { + // Step execution row id for the active waitForInput pause (polling uses includeInput: false) + const waitingStepExecutionId = useMemo(() => { if (!workflowExecution || workflowExecution.status !== ExecutionStatus.WAITING_FOR_INPUT) { return undefined; } @@ -124,7 +124,7 @@ export const WorkflowExecutionDetail: React.FC = R // consistent with every other step types const { data: pausedStepFullData } = useStepExecution( executionId, - pausedStepId, + waitingStepExecutionId, ExecutionStatus.WAITING_FOR_INPUT ); @@ -287,6 +287,7 @@ export const WorkflowExecutionDetail: React.FC = R resumeMessage={resumeMessage} resumeSchema={resumeSchema} shouldAutoResume={shouldAutoResume} + waitingStepExecutionId={waitingStepExecutionId} childWorkflowExecution={selectedStepChildExecution} parentWorkflowExecution={parentWorkflowExecution} /> diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_overview.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_overview.tsx index b0984dc87db2a..4056c609f60e1 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_overview.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_execution_overview.tsx @@ -29,6 +29,7 @@ interface WorkflowExecutionOverviewProps { resumeMessage?: string; resumeSchema?: JsonModelSchemaType; shouldAutoResume?: boolean; + waitingStepExecutionId?: string; } const formatExecutionDate = (date: string) => { @@ -60,6 +61,7 @@ export const WorkflowExecutionOverview = React.memo { const { euiTheme } = useEuiTheme(); @@ -199,6 +201,7 @@ export const WorkflowExecutionOverview = React.memo )} diff --git a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_details.tsx b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_details.tsx index 642fe822a9dc2..e1eae2d028aec 100644 --- a/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_details.tsx +++ b/src/platform/plugins/shared/workflows_management/public/features/workflow_execution_detail/ui/workflow_step_execution_details.tsx @@ -43,6 +43,7 @@ interface WorkflowStepExecutionDetailsProps { resumeMessage?: string; resumeSchema?: JsonModelSchemaType; shouldAutoResume?: boolean; + waitingStepExecutionId?: string; /** When the step is workflow.execute, the child workflow execution (to link to) */ childWorkflowExecution?: ChildWorkflowExecutionItem; /** When viewing a step that belongs to a nested execution, the parent workflow execution (to link to) */ @@ -59,6 +60,7 @@ export const WorkflowStepExecutionDetails = React.memo { @@ -187,6 +189,7 @@ export const WorkflowStepExecutionDetails = React.memo ); } @@ -281,6 +284,7 @@ export const WorkflowStepExecutionDetails = React.memo diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.test.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.test.ts index 6811e4e4ccb95..f129192e018cc 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.test.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.test.ts @@ -155,7 +155,12 @@ describe('getExecutionState', () => { ], }, stepExecutions: [ - { stepId: 'approve', status: ExecutionStatus.WAITING_FOR_INPUT, scopeStack: [] }, + { + id: 'step-exec-approve', + stepId: 'approve', + status: ExecutionStatus.WAITING_FOR_INPUT, + scopeStack: [], + }, ], }); @@ -167,6 +172,7 @@ describe('getExecutionState', () => { expect(state?.status).toBe(ExecutionStatus.WAITING_FOR_INPUT); expect(state?.waiting_input).toEqual({ + step_execution_id: 'step-exec-approve', message: 'Please approve this request', schema: { type: 'object', @@ -196,7 +202,12 @@ describe('getExecutionState', () => { ], }, stepExecutions: [ - { stepId: 'ask', status: ExecutionStatus.WAITING_FOR_INPUT, scopeStack: [] }, + { + id: 'step-exec-ask', + stepId: 'ask', + status: ExecutionStatus.WAITING_FOR_INPUT, + scopeStack: [], + }, ], }); @@ -207,6 +218,7 @@ describe('getExecutionState', () => { }); expect(state?.waiting_input).toEqual({ + step_execution_id: 'step-exec-ask', message: 'Provide input', }); }); @@ -235,7 +247,12 @@ describe('getExecutionState', () => { ], }, stepExecutions: [ - { stepId: 'nested_ask', status: ExecutionStatus.WAITING_FOR_INPUT, scopeStack: [] }, + { + id: 'step-exec-nested', + stepId: 'nested_ask', + status: ExecutionStatus.WAITING_FOR_INPUT, + scopeStack: [], + }, ], }); @@ -245,6 +262,51 @@ describe('getExecutionState', () => { workflowApi, }); + expect(state?.waiting_input).toEqual({ + step_execution_id: 'step-exec-nested', + message: 'Approve item', + }); + }); + + it('uses the waiting step execution id when the same stepId has a prior completed instance (loop)', async () => { + const workflowApi = createWorkflowApi(); + workflowApi.getWorkflowExecution = jest.fn().mockResolvedValue({ + status: ExecutionStatus.WAITING_FOR_INPUT, + workflowId: 'wf-loop', + startedAt: '2026-01-01T00:00:00.000Z', + workflowDefinition: { + name: 'Loop ask', + steps: [ + { + name: 'ask_for_input', + type: 'waitForInput', + with: { message: 'Approve item' }, + }, + ], + }, + stepExecutions: [ + { + id: 'step-exec-iter-0-done', + stepId: 'ask_for_input', + status: ExecutionStatus.COMPLETED, + scopeStack: [], + }, + { + id: 'step-exec-iter-1-waiting', + stepId: 'ask_for_input', + status: ExecutionStatus.WAITING_FOR_INPUT, + scopeStack: [], + }, + ], + }); + + const state = await getExecutionState({ + executionId: 'exec-loop', + spaceId: 'default', + workflowApi, + }); + + expect(state?.waiting_input?.step_execution_id).toBe('step-exec-iter-1-waiting'); expect(state?.waiting_input?.message).toBe('Approve item'); }); diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.ts index 4ad2e9b0da78c..cb266f8aa0444 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-genai-utils/tools/utils/workflows/get_execution_state.ts @@ -14,6 +14,8 @@ import { getWorkflowOutput } from './get_workflow_output'; type WorkflowApi = WorkflowsServerPluginSetup['management']; export interface WaitingInputContext { + /** Step execution document id for the paused `waitForInput` instance. Compare across status polls. */ + step_execution_id: string; /** Human-readable prompt from the waitForInput step's `with.message`. */ message?: string; /** JSON Schema describing the expected input, from the step's `with.schema`. */ @@ -30,7 +32,7 @@ export interface WorkflowExecutionState { workflow_name?: string; /** Present when status is FAILED; contains the workflow error message. */ error_message?: string; - /** Present when status is WAITING_FOR_INPUT; describes what input is needed to resume. */ + /** Present when status is WAITING_FOR_INPUT. */ waiting_input?: WaitingInputContext; } @@ -83,12 +85,11 @@ export const getExecutionState = async ({ const stepConfig = step?.type === 'waitForInput' ? (step as WaitForInputStep).with : undefined; const waitContext: WaitingInputContext = { + step_execution_id: waitingStep.id, ...(stepConfig?.message && { message: stepConfig.message }), ...(stepConfig?.schema && { schema: stepConfig.schema as Record }), }; - if (waitContext.message !== undefined || waitContext.schema !== undefined) { - state.waiting_input = waitContext; - } + state.waiting_input = waitContext; } } diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/tools/tool_types/workflow/tool_type.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/tools/tool_types/workflow/tool_type.ts index 9249d08dc6abe..1abd5b21e0ead 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/tools/tool_types/workflow/tool_type.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/tools/tool_types/workflow/tool_type.ts @@ -100,8 +100,8 @@ export const getWorkflowToolType = ({ - If the workflow wasn't completed, a workflow execution ID will be returned. - The ${platformCoreTools.getWorkflowExecutionStatus} tool can be used later to check the status of the workflow execution. - If the workflow returns with status "waiting_for_input", it is paused and requires human input to continue. - The response will include a "waiting_input" object with a "message" (what the workflow is asking for) - and an optional "schema" (JSON Schema describing the expected input fields). + The response will include a "waiting_input" object with "step_execution_id" (id of the paused step execution instance), + a "message" (what the workflow is asking for), and an optional "schema" (JSON Schema describing the expected input fields). Use the ${platformCoreTools.resumeWorkflowExecution} tool to provide the required input and resume the workflow. `); diff --git a/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/resume_workflow_execution.ts b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/resume_workflow_execution.ts index ae0c7dbec8ebe..d42ccd967c88d 100644 --- a/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/resume_workflow_execution.ts +++ b/x-pack/platform/plugins/shared/agent_builder_platform/server/tools/resume_workflow_execution.ts @@ -41,9 +41,14 @@ export const resumeWorkflowExecutionTool = ({ Use this tool when a workflow execution returned with status "waiting_for_input". The execution's "waiting_input" field describes what input is expected (message and optional schema). - Provide the executionId and the required input object to resume the workflow. - After resuming, use ${platformCoreTools.getWorkflowExecutionStatus} to check whether the workflow completed or paused again (e.g. inside a loop). + + **Important - read after resume:** a successful call means resume was **accepted and scheduled** in the engine. + The execution record can **lag**: you may still briefly see the same "waiting_for_input" and the same form right after this tool returns. **Do not** tell the user there is another HITL step, a loop or a second approval round unless you have **confirmed** a genuinely new pause (clear evidence such as a new step, changed context, or repeated status checks that rule out a stale snapshot). + + **What you should do:** call ${platformCoreTools.getWorkflowExecutionStatus} on the same executionId. Compare **waiting_input.step_execution_id** between polls: if it **changes** while still waiting_for_input, that is evidence of a **new** HITL step; if it is the **same** id immediately after resume, treat it as likely stale until status moves on. Stop once the status clearly updates (e.g. completed, failed, running) or after **up to ~5 status polls** without any change. + + **If status has not changed after those polls:** tell the user their resume was **submitted**, but you **could not confirm** the new execution state from Kibana yet - do **not** invent a second approval workflow. `), schema: resumeWorkflowExecutionSchema, handler: async ({ executionId, input }, { spaceId, request }) => {