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 }) => {