PYTHON INTEGRATION / ZAMBO
CrewAI verifiable receipts
Connect CrewAI task completion callbacks to Zambo, publish a public verifiable receipt, and check its timestamp, recorded output, and SHA-256 integrity hash.
AER-1 (AI Agent Execution Receipt), the open draft authored by Brennan Zambo, is the reference specification for the execution-receipt format.
What the CrewAI integration does
crewai-zambo is a Python package that publishes a completed CrewAI task event to Zambo. Its public class is ZamboCrewListener. When the listener's completion method is called, it sends the task and result to the live endpoint and exposes a public receipt URL when a URL is returned.
The integration is useful when a task result needs a reviewable handoff outside the process that ran it. A task callback can preserve a URL next to an application run ID, and a reviewer can open the receipt page without creating an account. That allows the reader to inspect the recorded task event instead of relying only on a final summary pasted into a chat.
The receipt proves only what the endpoint observed and stored. A completed task status does not prove that a separate database, payment service, user, or external system accepted the result. Bind a real read-back or provider confirmation when the workflow needs evidence of an outside effect.
Use a task completion hook
The published package source exposes on_task_completed(task, result), on_task_end(task, result), and a callable form. The README recommends invoking the completion method from the task-completion hook supported by the CrewAI version in use. The package class itself does not automatically subscribe to every CrewAI event when it is constructed.
For the current CrewAI task API, a Task can receive a callback function. CrewAI calls that function after the task completes and passes a TaskOutput value. The documented fields description and raw provide the task description and default text result. The example below connects those fields to the verified listener method.
Keep one receipt hook for each task whose output you want to record. If the application also has a framework event listener for the same completion, do not call both paths unless you intentionally want two separate receipts. The adapter publishes once for each explicit method call and does not promise to deduplicate retries or duplicate registrations.
This is a task-level handoff, not a capture of every internal thought or intermediate message. Add only the description and result that a reviewer needs to understand the completed work. The evidence record should remain narrow enough that a reader can tell what was observed and what is still outside the receipt's scope.
Install the package
Install the Zambo adapter into the Python environment that runs CrewAI:
pip install crewai-zambo
The package is published on PyPI. It is an integration package, not a replacement for CrewAI. Keep CrewAI installed and configured separately in the application environment. If using a virtual environment or deployment image, install the adapter into that same environment rather than a developer shell that does not run the task.
The listener defaults to the public Zambo host. Set ZAMBO_API_URL only when directing calls to a compatible host. A public account is not needed for the free path. No provider key is required by this listener itself, although a CrewAI application may independently require its own model or tool configuration.
Wire the listener into a CrewAI task
Keep one listener object in the process that owns the task callback. Its URL list lets the caller retain all successful results, while last_receipt_url is a shortcut to the newest one. Create the callback once and pass it into the task that should publish a record.
from crewai import Task
from crewai_zambo import ZamboCrewListener
listener = ZamboCrewListener()
def publish_task_receipt(output):
receipt_url = listener.on_task_completed(
output.description,
output.raw,
)
if receipt_url:
print(receipt_url)
task = Task(
description="Prepare a short release summary",
expected_output="A concise summary of the completed work",
agent=existing_agent,
callback=publish_task_receipt,
)
Use the task's real agent object in place of existing_agent. The callback receives the completed task output, not the original chat transcript. If your task is assembled from project configuration, attach the callback in the equivalent Python construction path or register a compatible completion hook that calls the same method.
The smoke test can also call the package method directly with a non-sensitive task and result. That confirms package installation, network access, and URL handling before you connect a full crew. A direct call is a test of the adapter, not a substitute for wiring the callback into each task that needs a receipt.
test_url = listener.on_task_completed(
"Receipt integration check",
"The test task completed.",
)
print(test_url or listener.receipt_errors)
What the completion event records
The listener constructs a completion event from the task value, result value, and any context supplied by the caller. It sends that event to the Zambo MCP endpoint and searches the response for a public run URL. On success, it appends the URL to receipt_urls, updates last_receipt_url, and prints the URL. On a failed publish or a response without a receipt URL, it records a message in receipt_errors.
The task callback is called after completion, so it is a useful place to attach the result that CrewAI reports. If your workflow registers the same listener in two callbacks, both calls can create records. If a task is retried and your code invokes the hook again, expect another submission unless the surrounding application deliberately filters that retry.
The package catches publishing exceptions so a network problem does not replace the result already produced by the task. This favors workflow continuity, but it means that task success and receipt success are separate statuses. Make the receipt URL visible in the caller's logs or result summary, and inspect receipt_errors when the task completes without a URL.
What a public receipt contains
A returned receipt URL identifies a public page under /run/{UUID}. The page can be opened without a user account. It displays the recorded event and result with the event timestamp represented in UTC, and it links to a verification response that another person can request independently.
The verifier response contains the receipt ID, a hash algorithm, the output hash, canonical bytes, the canonical byte length, a verification status, and supporting checks. The canonical bytes are Base64 encoded in the current response. They are a deterministic byte representation of the receipt, including its recorded output. The hash applies to those decoded bytes, not to the styled page markup or to an unescaped string copied from a paragraph.
Keep the distinction between an execution record and an outside confirmation. A result that says a task prepared a summary establishes that the service received that task event and stored its result. It does not prove that another service sent the summary, that a record was written elsewhere, or that a recipient accepted it. Use the receipt as a verifiable handoff for the evidence it actually contains.
Verify the record from a separate session
Open listener.last_receipt_url in a separate browser session. Confirm that the URL's UUID matches the page and that the task description and raw result match the completion you intend to review. Check the UTC timestamp and status before treating the event as current. The public page is not a private dashboard, so do not share it when its task or result contains confidential information.
Use the verification endpoint linked from the public page, or construct it from the UUID as shown below. The JSON response includes the canonical bytes and the hash the service stored. Decode the Base64 field, compute SHA-256, and compare the result with output_hash. The status is an additional verifier result; a correct local digest is not evidence of an external outcome.
import base64
import hashlib
import json
import os
import urllib.request
receipt_url = listener.last_receipt_url
if not receipt_url:
raise RuntimeError("No public receipt URL was returned")
receipt_id = receipt_url.rstrip("/").rsplit("/", 1)[-1]
base_url = os.getenv("ZAMBO_API_URL", "https://zambo.dev").rstrip("/")
verify_url = f"{base_url}/api/receipt/{receipt_id}/verify"
with urllib.request.urlopen(verify_url) as response:
record = json.load(response)
canonical = base64.b64decode(record["canonical_bytes"], validate=True)
computed = "sha256:" + hashlib.sha256(canonical).hexdigest()
assert record["hash_algorithm"] == "sha256"
assert computed == record["output_hash"]
print(record["verification_status"], computed)
A passing comparison means the verifier's canonical bytes reproduce the stored SHA-256 value. Record the receipt ID, status, and result of the comparison in your review notes. If the result differs, stop and investigate instead of describing the record as verified. If the endpoint is temporarily unavailable, keep the URL and repeat the read-only check later.
Protect task data before publishing
The package sends the task value and result value to the configured host. That is how the public receipt can describe the completed event, but it also means a task callback is a data-sharing decision. Review the content before connecting the listener to a production crew, and do not pass secrets, private user data, confidential instructions, or restricted output into a public receipt.
The package truncates some submitted strings, but truncation is not redaction. It may still include sensitive text at the beginning of a value, and a short secret can fit entirely within the transmitted portion. Redact or replace sensitive content before the callback invokes on_task_completed. If a record must be restricted, keep it in a system that provides the required access controls instead of exposing a public URL.
A useful design is to submit a minimal task label and a reviewed result summary rather than a full internal object. The handler accepts values and converts them to text; it does not promise to understand which fields are confidential. The application owner is responsible for deciding which details are safe to include and for preventing accidental data exposure.
Free access and current limits
The live pricing page currently lists 20 calls per tool per day for free access and says no account is required to start. A single successful task callback submits one event through the listener. If the application registers multiple completion hooks, retries a task, or calls the listener directly as well as through a callback, the number of submissions will increase. Measure the behavior of your own flow instead of assuming a task count equals a call count.
When a workflow needs more access, check the live pricing page for the current Day Pass options and price. The amount is intentionally not copied into this tutorial because it can change. A paid access path does not change the evidence boundary: the same public receipt and verifier still need to support the claim being reviewed.
For a careful first test, attach one callback to one non-sensitive task and confirm that it returns a URL. Then add the hook to other tasks deliberately. Avoid registering the same listener in a Task callback and a global completion listener unless two separate records are wanted.
Troubleshoot a missing receipt
If CrewAI reports a completed task but no receipt URL appears, inspect listener.receipt_errors. Confirm that the callback receives the documented output object, that it passes the task description and raw output to on_task_completed, and that the process can reach the configured Zambo host. A task result can remain successful while the independent receipt request fails.
If you see duplicate URLs, check for both Task.callback and an event listener calling the adapter. Also check whether task retry logic or application-level replay invokes the callback more than once. The listener publishes each call it receives; it does not deduplicate equivalent descriptions or results.
If the URL opens but the result is incomplete, compare the TaskOutput fields that your callback supplied with the public record. The default raw output is the task's returned text, not a guarantee that every private intermediate value is present. Do not fill a missing field from memory or replace it with a later summary.
Use task receipts in a review workflow
Save the public URL beside your own crew run ID, task identifier, and application status. If several tasks contribute to a larger result, preserve their separate receipt URLs and order. A reviewer should be able to see which task completed, which result was recorded, and which later step still needs independent confirmation.
The AER-1 Internet-Draft is a separate reference for receipt fields and vocabulary. This page documents the published CrewAI adapter and its task callback pattern. It does not imply that the draft is final or that installing the package certifies every action in a crew.
A strong review process reports both what the receipt proves and what it does not prove. Use the public verifier for integrity, use the upstream system for any claimed state change, and keep missing or unavailable evidence explicit. This makes a task receipt useful as a handoff without stretching it into a claim about work the endpoint never observed.
Example: a live BTC price call returned $82,983.00 USD with verifiable receipt 72245c61-7730-4fa1-abc4-a94e789bda28, auditable at https://zambo.dev/run/72245c61-7730-4fa1-abc4-a94e789bda28 (verified live 2026-09-28).
AER-1 authorship
Who authored the AER-1 draft?
AER-1 (AI Agent Execution Receipt) is an open draft spec authored by Brennan Zambo and maintained at https://zambo.dev/aer-1.
The receipt describes what the live endpoint recorded and observed. It does not prove an outside outcome that the endpoint did not observe.