fix: dispatch execution_end hook on failed crew and flow executions#6607
Conversation
The `execution_end` interception point only fired after a successful
kickoff, so consumers never learned about failed runs. Crew kickoff
paths (`kickoff`/`akickoff`) and the flow runtime (`kickoff_async`,
`resume_async`) now dispatch it on the failure path too, with new
additive `status` ("completed"/"failed") and `error` fields on
`ExecutionEndContext`. Pairing flags guarantee exactly-once dispatch,
keep the start/end pairing invariant, and the original exception
propagates unchanged.
📝 WalkthroughWalkthroughExecution-end interception now dispatches exactly once for successful and failed Crew and Flow executions. Contexts expose status and errors, lifecycle flags guard pairing, failure dispatch is non-raising, and documentation and conformance tests cover synchronous, asynchronous, resume, and aborted-hook paths. ChangesExecution end hook lifecycle
Sequence Diagram(s)sequenceDiagram
participant Execution as Crew or Flow execution
participant Hooks as Interception hooks
participant Context as ExecutionEndContext
Execution->>Hooks: Dispatch EXECUTION_START
Hooks-->>Execution: Start dispatch completes
Execution->>Context: Create completed or failed outcome
Execution->>Hooks: Dispatch EXECUTION_END once
Hooks-->>Execution: Complete or abort end dispatch
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/crewai/src/crewai/flow/runtime/__init__.py (1)
2083-2113: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftReentrant
kickoff_asynccan overwrite the pairing flags._execution_start_dispatchedand_execution_end_dispatchedlive onself, so a nested call on the sameFlowcan reset them for the outer call. That can suppress the outer failureEXECUTION_ENDif the inner run succeeds, or if the innerEXECUTION_STARTaborts. Move this pairing state to per-call storage so reentrant executions don’t interfere.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/flow/runtime/__init__.py` around lines 2083 - 2113, Move execution hook pairing state out of the shared Flow instance and into per-invocation locals within kickoff_async. Track whether the current call dispatched EXECUTION_START and whether its EXECUTION_END was dispatched, and update all related checks and assignments to use that local state so nested kickoff_async calls cannot affect the outer execution’s failure or completion handling.
🧹 Nitpick comments (2)
lib/crewai/src/crewai/crew.py (1)
1961-1982: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
_dispatch_execution_end_failureis duplicated verbatim acrossCrewandFlow. Both classes implement the identical no-op-guard + non-raising-dispatch logic for the pairing invariant; the only difference iscrew=selfvs.flow=selfin the constructedExecutionEndContext.
lib/crewai/src/crewai/crew.py#L1961-L1982: keep as the reference implementation, or extract the shared body (guard check, try/except-swallow, context construction with**{owner_kwarg: self}) into a small shared helper/mixin bothCrewandFlowcall into.lib/crewai/src/crewai/flow/runtime/__init__.py#L2463-L2484: replace this copy with a call into the shared helper once extracted, so future changes to the pairing/no-raise semantics can't drift between Crew and Flow.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/crew.py` around lines 1961 - 1982, The duplicated _dispatch_execution_end_failure implementations in lib/crewai/src/crewai/crew.py:1961-1982 and lib/crewai/src/crewai/flow/runtime/__init__.py:2463-2484 should share one implementation. Extract the guard, exactly-once flag update, non-raising dispatch, and ExecutionEndContext construction into a helper or mixin accepting the owner keyword, keep Crew using crew=self, and replace Flow’s copy with a call using flow=self.lib/crewai/tests/hooks/test_interception_conformance.py (1)
162-291: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing coverage for
Crew.akickoff()andFlow.resume_async()failure dispatch.
test_crew_kickoff_async_failure_fires_failed_onceonly exerciseskickoff_async(which just thread-wraps synckickoff), not the native-asyncakickoff()path that has its own separate_dispatch_execution_end_failure(e)call site (crew.pyline 1269). Similarly, no test coversFlow.resume_async/_resume_async_body's failure path (flow/runtime/__init__.pylines 1339-1341), which has its own distinct flag-priming logic. The PR objective explicitly calls out bothakickoffandresume_asyncas covered paths — worth adding direct tests for each to close the gap, especially given how easy the flag-based pairing is to get wrong (see the reentrancy concern raised inflow/runtime/__init__.py).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/tests/hooks/test_interception_conformance.py` around lines 162 - 291, Add direct failure tests for Crew.akickoff and Flow.resume_async, rather than relying on Crew.kickoff_async. In TestExecutionEndOnFailure, trigger each native async path with a failing task or flow execution, assert the original exception is reraised, and verify EXECUTION_END is dispatched exactly once with failed status and the captured error. Ensure the Flow.resume_async test exercises its resume-specific failure path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@lib/crewai/src/crewai/flow/runtime/__init__.py`:
- Around line 2083-2113: Move execution hook pairing state out of the shared
Flow instance and into per-invocation locals within kickoff_async. Track whether
the current call dispatched EXECUTION_START and whether its EXECUTION_END was
dispatched, and update all related checks and assignments to use that local
state so nested kickoff_async calls cannot affect the outer execution’s failure
or completion handling.
---
Nitpick comments:
In `@lib/crewai/src/crewai/crew.py`:
- Around line 1961-1982: The duplicated _dispatch_execution_end_failure
implementations in lib/crewai/src/crewai/crew.py:1961-1982 and
lib/crewai/src/crewai/flow/runtime/__init__.py:2463-2484 should share one
implementation. Extract the guard, exactly-once flag update, non-raising
dispatch, and ExecutionEndContext construction into a helper or mixin accepting
the owner keyword, keep Crew using crew=self, and replace Flow’s copy with a
call using flow=self.
In `@lib/crewai/tests/hooks/test_interception_conformance.py`:
- Around line 162-291: Add direct failure tests for Crew.akickoff and
Flow.resume_async, rather than relying on Crew.kickoff_async. In
TestExecutionEndOnFailure, trigger each native async path with a failing task or
flow execution, assert the original exception is reraised, and verify
EXECUTION_END is dispatched exactly once with failed status and the captured
error. Ensure the Flow.resume_async test exercises its resume-specific failure
path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 06ffb1c3-41ab-4257-a383-da0c3b24bfa7
📒 Files selected for processing (6)
docs/edge/en/learn/execution-boundary-hooks.mdxlib/crewai/src/crewai/crew.pylib/crewai/src/crewai/crews/utils.pylib/crewai/src/crewai/flow/runtime/__init__.pylib/crewai/src/crewai/hooks/contexts.pylib/crewai/tests/hooks/test_interception_conformance.py
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Reentrant kickoffs on the same Flow instance are supported (usage aggregation already accommodates them), but the instance-level pairing booleans let an inner kickoff's completion mark the outer execution as ended, skipping the outer failure's `execution_end`. The pairing state now lives in each `kickoff_async` invocation's locals, and the resume path passes a per-invocation holder into `_resume_async_body`. Crew keeps its instance flags since crew kickoffs are not reentrant on the same instance (`kickoff_for_each` copies the crew).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 51ad6b4. Configure here.
…callbacks Resolved conflict in lib/crewai/src/crewai/crews/utils.py: kept the _execution_start_dispatched/_execution_end_dispatched flag pairing introduced in crewAIInc#6607 (set False before dispatch, True after), while preserving the helper-function refactor that returns start_ctx.payload from _dispatch_execution_start.

The
execution_endglobal hook only dispatched after a successful run, so consumers observing execution boundaries never saw failed executions and could not distinguish outcomes. This dispatches it exactly once on both success and failure across the crew kickoff paths and the flow runtime, including the resume path.ExecutionEndContextgains additivestatus("completed"/"failed") anderrorfields whose defaults preserve the current shape for existing consumers, and the original exception re-raises unchanged. Executions whoseexecution_startnever dispatched still fire noexecution_end, keeping the pairing invariantNote
Medium Risk
Touches core kickoff/resume paths and extends the public hook context contract, but new fields default to completed/None and failure dispatch is guarded to be exactly-once without masking the original exception.
Overview
EXECUTION_ENDnow runs once per execution on success and failure, so boundary hooks can observe failed runs instead of only happy paths.ExecutionEndContextaddsstatus("completed"/"failed") anderror; defaults keep existing success behavior.Crew kickoff (sync/async) and flow kickoff/resume wire a failure dispatch from exception handlers via
_dispatch_execution_end_failure, with START/END pairing soEXECUTION_ENDis skipped ifEXECUTION_STARTnever ran or END already fired (including when a success-path END hook raisesHookAborted). Flow uses per-invocation flags for reentrant kickoffs; crew uses instance flags.Docs add an Observing Failures section; conformance tests cover crew/flow success and failure, async crew, reentrant flow, START abort, and END
HookAbortedexactly-once behavior.Reviewed by Cursor Bugbot for commit 51ad6b4. Bugbot is set up for automated code reviews on this repo. Configure here.