Project: Real CrewAI Persistent Flow

Project objective

This layer runs crewai==1.15.4 directly, still without model calls or an API key. It validates real Flow, @start(), @router(), @listen(), @persist(…), and SQLiteFlowPersistence, and injects the most dangerous recovery window: the business effect has committed but Flow state has not yet been saved. PyPI’s latest stable package observation on 2026-07-21 was 1.15.5; this project does not confuse “latest installable” with “validated version.”

Layer A: Offline Research Brief remains responsible for framework-independent Tasks, events, revision budget, and publication contracts. This project does not replace it.

Why state ID and operation ID are separate

IdentifierManaged byUse
state.idCrewAI Flow state UUIDFind one persisted execution lineage
operation_idCaller, from business intentIdempotency, receipt, and conflict detection
payload_hashApplication, from normalized payloadPrevent one operation ID from being used for different content

Layer A originally called its deterministic fingerprint run_id, which could imply a unique UUID for each execution. It is now named operation_id. Real Layer B keeps both a framework UUID and a business idempotency key.

The responsibilities of two SQLite databases

flowchart LR
    A["start: validate schema, topic, operation_id"] --> B["router: route_execute"]
    B --> C["ReceiptStore.apply_once"]
    C --> D[("effect-receipts.sqlite3")]
    C -. "inject crash: receipt committed" .-> X["process failure"]
    A -. "@persist after method completes" .-> S[("flow-state.sqlite3")]
    X --> R["new process: resume same state.id"]
    R --> C
    C -->|"matching payload receipt found"| E["reuse result; effect_count remains 1"]
    E --> F["stage=done; then save Flow state"]

Flow persistence and a business receipt are different concepts. @persist saves state after a method completes successfully. If the effect commits and the method then raises, current state can still be prepared. Recovery must query the receipt before serial replay, or it can produce a second effect.

This project has one @start(); it proves idempotent receipt behavior only under single-process, serial recovery. It makes no assertion about scheduling of several satisfied starts. Multi-worker concurrency, remote APIs, database isolation levels, and cross-region failure need separate validation.

Pinned-version API facts

  • @persist is an invoked decorator: use @persist() or @persist(SQLiteFlowPersistence(path)). In 1.15.4, bare @persist replaces the class with a decorator function.
  • Route labels and listener method names must be separate. This project uses route_execute/route_done so a handler cannot listen to itself.
  • kickoff(inputs={“id”: uuid}) hydrates the latest state under the same UUID and then re-enters satisfied @start() graph nodes; it does not automatically skip individual nodes.
  • kickoff(restore_from_state_id=uuid) forks from a snapshot and assigns a new state.id.
  • An unknown UUID may silently begin a new Flow. The wrapper checks persistence before invoking real kickoff for both resume and fork; unknown IDs fail closed.

Checkpointing is another capability. Pinned-version Flow.from_checkpoint requires CheckpointConfig; it must not be conflated with @persist hydration, and early-release/best-effort checkpoints cannot replace a business receipt.

Files and environment

examples/crewai_layer_b/
├── crewai_persistent_flow.py
├── requirements.txt
└── test_crewai_persistent_flow.py

Run from that directory:

$practice = Join-Path $env:TEMP ("crewai-layer-b-{0}" -f [guid]::NewGuid())  # Create a unique temporary environment outside the vault.
py -3.11 -m venv $practice  # Create an isolated environment with course-revalidated Python 3.11.
$python = Join-Path $practice "Scripts\python.exe"  # Keep the absolute interpreter path instead of relying on shell activation.
& $python -m pip install --upgrade pip  # Upgrade pip in the temporary environment for the known resolver prerequisite.
& $python -m pip install --requirement .\requirements.txt  # Install the real CrewAI runtime from the direct dependency list.
& $python -m pip check  # Verify no unsatisfied requirements remain.
$env:PYTHONUTF8 = "1"  # Force UTF-8 for output that can contain emoji.
$env:CREWAI_DISABLE_TELEMETRY = "true"  # Request that this test process disable CrewAI telemetry.
$env:CREWAI_TESTING = "true"  # Enable the pinned-version isolation switch used by automated tests.
$env:OTEL_SDK_DISABLED = "true"  # Disable OpenTelemetry SDK instrumentation in this test process.
$env:DO_NOT_TRACK = "1"  # Declare non-tracking intent; production still needs independent egress verification.

PYTHONUTF8=1 avoids Windows default GBK failing to encode emoji in CrewAI event output. CREWAI_TESTING is an internal isolation switch used by the pinned version’s automated tests, not a production configuration. OTEL_SDK_DISABLED and DO_NOT_TRACK are likewise only for this isolated test process; they do not replace deployment egress audit. A non-test service must explicitly perform noninteractive trace opt-out and verify outbound networking and telemetry.

When verified on 2026-07-20, a new Python 3.11 virtual environment with bundled pip 24.0 failed resolving jsonschema → rpds-py. Upgrading pip inside that temporary environment to 26.1.2 let the same crewai==1.15.4 install and pip check pass, making the upgrade a reproducible prerequisite. requirements.txt pins one direct dependency, not a complete lockfile. That resolution contained 138 distributions, occupied about 800 MB, and included transitive ChromaDB, LanceDB, MCP, OpenAI SDK, and OpenTelemetry dependencies. A production project must trim for its actual feature set, create a complete hash-locked manifest, and perform license/supply-chain scanning; do not treat the teaching environment as a minimal deployment.

Normal run, same-lineage recovery, and fork

$stateDb = Join-Path $env:TEMP ("crewai-state-{0}.sqlite3" -f [guid]::NewGuid())  # Create a unique Flow-state SQLite path.
$effectDb = Join-Path $env:TEMP ("crewai-effect-{0}.sqlite3" -f [guid]::NewGuid())  # Create a separate effect-ledger SQLite path.
$fresh = (& $python -B .\crewai_persistent_flow.py --state-db $stateDb --effect-db $effectDb start --topic "Agent reliability" --operation-id "publish-001") | ConvertFrom-Json  # Start a lineage and retain its Flow UUID.
& $python -B .\crewai_persistent_flow.py --state-db $stateDb --effect-db $effectDb resume --flow-id $fresh.flow_id  # Resume the same persisted snapshot.
& $python -B .\crewai_persistent_flow.py --state-db $stateDb --effect-db $effectDb fork --flow-id $fresh.flow_id  # Fork a separate runtime lineage from it.

A normal run returns stage=done, the CrewAI UUID, business operation_id, receipt, and effect_count=1. Same-lineage recovery retains the UUID. A fork gets a new UUID, but hydrates the same completed snapshot and does not resubmit the effect.

Inject a crash after the receipt

$failureLine = & $python -B .\crewai_persistent_flow.py --state-db $stateDb --effect-db $effectDb start --topic "Failure recovery" --operation-id "publish-002" --crash-after-receipt 2>&1 | Select-Object -Last 1  # Keep the final JSON failure payload.
$failure = $failureLine | ConvertFrom-Json  # Obtain the Flow UUID for inspection and recovery.
& $python -B .\crewai_persistent_flow.py --state-db $stateDb --effect-db $effectDb inspect --flow-id $failure.flow_id  # Inspect the crashed state without mutation.
& $python -B .\crewai_persistent_flow.py --state-db $stateDb --effect-db $effectDb resume --flow-id $failure.flow_id  # Resume and recover receipt rather than duplicate the effect.

The failing process returns exit code 3. Inspect should show Flow state still prepared, while the effect ledger is already 1. After recovery in a new process, recovered_receipt=true, stage=done, and effect count remains 1.

Run the nine real CrewAI tests

& $python -B -m unittest -v test_crewai_persistent_flow.py  # Normal-mode regression tests.
& $python -B -O -m unittest -v test_crewai_persistent_flow.py  # Ensure checks do not rely on optimization-removed bare assert.
$env:PYTHONWARNINGS = "error"  # Make warnings not explicitly allowed fail immediately.
& $python -B -m unittest -v test_crewai_persistent_flow.py  # Strict-warning mode.
& $python -B -O -m unittest -v test_crewai_persistent_flow.py  # Optimization plus strict warnings.
Remove-Item Env:PYTHONWARNINGS  # Clear this session-only policy.

The suite covers the pinned version, no-key routing, old-schema rejection, same-UUID hydration, normalization of leading/trailing whitespace around flow_id, forks, fail-closed unknown UUIDs in both resume and fork, post-receipt crash recovery, and two independent processes. It also proves one operation_id cannot bind different payloads: conflict causes no second effect. It checks stderr for absence of Windows codec/CrewAIEventsBus errors. Transitive 1.15.4 dependencies emit one OpenTelemetry SelectableGroups deprecation and one crewai.rag ImportWarning on Python 3.11. The tests allowlist only those two full messages; all other warnings still fail under -W error. Review them again during upgrades rather than broadening ignores.

In isolated revalidation on 2026-07-21, all nine tests passed once in normal, -O, -W error, and -O -W error modes. Logs showed no API, LLM, HTTP, or telemetry errors, but no packet capture was performed. This proves only the current code and logging boundary; it does not prove absolute zero network connection.

Telemetry, AMP tracing, and application logs

Govern these channels independently:

  • CREWAI_DISABLE_TELEMETRY=true disables CrewAI anonymous telemetry only and is preferable to globally disabling OpenTelemetry for one library.
  • Flow(…, tracing=False) is this project’s explicit AMP-tracing override in 1.15.4. That version treats only CREWAI_TRACING_ENABLED=true/1 as an enable signal; an environment value of false can still read locally saved trace_consent, so it is not reliable opt-out. Production must manage persistent consent through the official CLI and verify actual egress.
  • OTEL_SDK_DISABLED=true disables all OpenTelemetry instrumentation in the same process and should be used only when global shutdown is required.
  • suppress_flow_events=True disables Flow and method lifecycle event emission, reducing local listeners and observability. This fixture uses it to isolate recovery tests; do not copy it into a Flow that needs event audit. Redirecting logs changes only local output. Neither setting proves anonymous telemetry is disabled.

The fixture uses the CrewAI-specific variable for anonymous telemetry and sets tracing=False on every Flow. Production must not rely on CREWAI_TESTING, and a lack of outbound traffic in a test must not imply that deployment images, plug-ins, or exporters are equally safe.

Acceptance checklist

  • Explain the different responsibilities of state UUID, operation ID, and payload hash.
  • Same-UUID recovery and fork have the expected ID/history semantics.
  • The application rejects unknown UUIDs rather than silently creating a Flow.
  • A crash after receipt commit then recovery retains a serial effect count of 1.
  • Explain the difference between @persist hydration, checkpoint skipping, and external receipts.
  • Windows tests have no encoding error and anonymous telemetry and AMP tracing are configured separately.
  • Do not extrapolate local SQLite results to concurrent or distributed exactly-once behavior.

Back to the index

Return to CrewAI Learning Index, then decide whether the project needs real Agents, Tasks, Crews, and a model provider.

Primary references

Official documentation, 1.15.4 wheel API, and isolated runtime verification were checked on 2026-07-21.