Description
Crew.replay() restores previously stored task outputs onto the current task list by position, and nothing checks that position i in the stored log is the same task as position i in the crew. If the task list changed between the run and the replay — which is the workflow replay exists for — outputs land on the wrong tasks and execution can resume from the wrong one, with no error.
The same file already does this correctly on the other recovery path: _restore_runtime() matches by task.id.
Steps to Reproduce
- Run a crew of three tasks:
RESEARCH, WRITE, REVIEW.
- Add one task at the front, so the crew is now
PLAN, RESEARCH, WRITE, REVIEW.
- Replay from the stored
task_id of REVIEW.
Expected behavior
Either resume at REVIEW with each restored output on the task it came from, or refuse with an error because the stored log no longer corresponds to the crew.
Screenshots/Code snippets
from crewai import Agent, Task, Crew
from crewai.tasks.task_output import TaskOutput
def mk(d):
a = Agent(role=f"r-{d}", goal="g", backstory="b", llm="gpt-4o-mini")
return Task(description=d, expected_output="o", agent=a)
# run 1
t = [mk("RESEARCH"), mk("WRITE"), mk("REVIEW")]
c1 = Crew(agents=[x.agent for x in t], tasks=t)
c1._task_output_handler.reset()
for i, x in enumerate(t):
c1._store_execution_log(
x, TaskOutput(description=x.description, raw=f"OUT-{x.description}",
agent=x.agent.role), i)
target = c1._task_output_handler.load()[2]["task_id"] # REVIEW
# run 2 — one task added at the front
t2 = [mk(d) for d in ["PLAN", "RESEARCH", "WRITE", "REVIEW"]]
c2 = Crew(agents=[x.agent for x in t2], tasks=t2)
def show(tasks, start_index=None, was_replayed=False, *a, **k):
print("resumes at:", tasks[start_index].description)
for x in tasks:
print(f" {x.description:9s} <- {x.output.raw if x.output else None}")
raise SystemExit
c2._execute_tasks = show
c2.replay(task_id=target)
Output:
resumes at: WRITE
PLAN <- OUT-RESEARCH
RESEARCH <- OUT-WRITE
WRITE <- None
REVIEW <- None
The same script with the crew left unchanged resumes at REVIEW with zero misplaced outputs, so this is the changed task list rather than the harness:
unchanged crew resumes at REVIEW misplaced: 0
task added at front resumes at WRITE misplaced: 2
tasks reordered resumes at REVIEW misplaced: 2
The reordering row is the quieter one: it resumes at the right task by coincidence of position, while the restored context is shuffled, so the run looks normal and the result is subtly wrong.
Evidence
crew.py, in replay():
start_index = self._find_task_index(task_id, stored_outputs) # index into the LOG
for i in range(start_index):
stored_output = stored_outputs[i]["output"]
...
self.tasks[i].output = task_output # applied to the CREW
return self._execute_tasks(self.tasks, start_index, True)
task_id is used only to find a position in the log; from there everything is positional.
It cannot currently validate the pairing, because ids are not stable across runs — task.py:
id: UUID4 = Field(default_factory=uuid.uuid4, frozen=True, ...)
The checkpoint path in the same file does match on identity:
# _restore_runtime()
if task.output is not None or str(task.id) not in started_task_ids:
continue
Possible Solution
Store something stable alongside task_index — the task's description, or a user-settable task key — and on replay compare it with self.tasks[i] before assigning, raising when they disagree. That turns a silent wrong-context run into an explicit "this log does not match this crew". A user-settable stable key would additionally make replay work across edits, which is the case replay is most useful for.
Operating System
macOS (Darwin 25.5.0, arm64)
Python Version
3.12
crewAI Version
1.15.18
crewAI Tools Version
not installed
Virtual Environment
Venv
Additional context
Reported alongside a related check on other durable-execution runtimes; this one is specific to replay() and independent of #5802.
Description
Crew.replay()restores previously stored task outputs onto the current task list by position, and nothing checks that positioniin the stored log is the same task as positioniin the crew. If the task list changed between the run and the replay — which is the workflow replay exists for — outputs land on the wrong tasks and execution can resume from the wrong one, with no error.The same file already does this correctly on the other recovery path:
_restore_runtime()matches bytask.id.Steps to Reproduce
RESEARCH,WRITE,REVIEW.PLAN,RESEARCH,WRITE,REVIEW.task_idofREVIEW.Expected behavior
Either resume at
REVIEWwith each restored output on the task it came from, or refuse with an error because the stored log no longer corresponds to the crew.Screenshots/Code snippets
Output:
The same script with the crew left unchanged resumes at
REVIEWwith zero misplaced outputs, so this is the changed task list rather than the harness:The reordering row is the quieter one: it resumes at the right task by coincidence of position, while the restored context is shuffled, so the run looks normal and the result is subtly wrong.
Evidence
crew.py, inreplay():task_idis used only to find a position in the log; from there everything is positional.It cannot currently validate the pairing, because ids are not stable across runs —
task.py:The checkpoint path in the same file does match on identity:
Possible Solution
Store something stable alongside
task_index— the task'sdescription, or a user-settable task key — and on replay compare it withself.tasks[i]before assigning, raising when they disagree. That turns a silent wrong-context run into an explicit "this log does not match this crew". A user-settable stable key would additionally make replay work across edits, which is the case replay is most useful for.Operating System
macOS (Darwin 25.5.0, arm64)
Python Version
3.12
crewAI Version
1.15.18
crewAI Tools Version
not installed
Virtual Environment
Venv
Additional context
Reported alongside a related check on other durable-execution runtimes; this one is specific to
replay()and independent of #5802.