Skip to content
Zarif Automates

Build a Ticket Workflow: Signed Delivery, Evidence and Human Approval

ZarifZarif
|Published

A refund question arrives twice. Your workflow finds the billing policy and proposes a label. Someone approves it, the database changes, and the caller loses track of the result. The next request must recover the recorded outcome without applying the change again.

This capstone connects the course's delivery, retrieval and approval work into one runnable local workflow. It uses real HTTP and SQLite. Model responses are authored fixtures, the signing key is deliberately public, and the reviewer is a fixed synthetic identity. You can inspect every handoff without buying API access or connecting a customer system.

The result is a tested application boundary around a simulated model response. It is not a deployed autonomous agent or evidence of live model quality.

Set up the complete artifact

You need Python 3.10 or newer and a terminal. The example uses only the standard library. Complete the webhook, RAG and approval lessons first if their boundaries are unfamiliar.

Download and extract the ticket workflow capstone. Its files have separate jobs:

FileRole
capstone.pyHTTP delivery, inbox, preparation and command-line workflow
policies.jsonFour routing documents, including foreign-owner and inactive distractors
model-output-fixtures.jsonAn authored billing proposal with a citation; no model call
approval_lesson.pyExact approval core from the earlier lesson
rag_lesson.pyExact retrieval module from the RAG lesson
verify_capstone.pyTwelve integrated scenarios and source hashes
observed-results.jsonRecorded run for comparison

Run the full suite:

python3 verify_capstone.py > my-results.json

Use python if that is your installed command. The observed run used Python 3.12.14 and reported scenarios: 12, passed: 12, and model_requests: 0. Each scenario uses a temporary database; ingress is an actual loopback HTTP request. A failed assertion exits nonzero rather than producing a passing report.

Compare my-results.json with observed-results.json: the scenario names, outcomes and source_sha256 values should match. Your python version field may differ. A source-hash difference means you are testing different bytes; investigate that before interpreting an outcome difference as a workflow regression.

Follow one event through the system

The application follows this sequence:

Signed HTTP body
  -> validated, durable inbox row
  -> owner-scoped policy retrieval
  -> authored proposal + citation validation
  -> stored exact proposal and pending approval
  -> explicit reviewer decision
  -> version-checked ticket update + receipt + audit event

These are connected stages operating on the same event and database. The verifier does not merely run the earlier lesson scripts beside each other. It reuses their approval and retrieval functions where those contracts fit.

The event contains an ID, ticket.created, ticket T100, and the question “Where should this refund request go?” It cannot supply an owner, reviewer, database path or tool name. The application chooses owner demo-a and limits the exercise to that one ticket. That restriction is a fixture scope, not user authentication.

Prepare the proposal without applying it

Start a fresh example:

python3 capstone.py prepare --db demo.sqlite
python3 capstone.py inspect --db demo.sqlite

Prepare refuses to replace an existing database. Use a different filename for a new attempt. It starts a short-lived receiver on 127.0.0.1, sends the signed event, stops the receiver and processes the accepted work. It leaves a pending approval; the ticket is still unassigned at version 1 and there are zero receipts.

The delivery response is HTTP 202 with state received. The local protocol signs timestamp, a period and the exact body bytes using HMAC-SHA256. Timestamp freshness and a constant-time signature comparison happen before JSON parsing. See Python's HMAC documentation for the comparison primitive. This is the course's own protocol, not a GitHub or Stripe adapter.

The inbox stores the raw body and its hash before acknowledging acceptance. The same event ID and body return HTTP 200 with duplicate: true. Reusing the ID for different signed content returns 409. It does not replace the original question.

Because the example key is public, anyone who knows this artifact can construct a valid signature. It demonstrates verification mechanics, not protection of a deployed endpoint. The receiver binds only to loopback and runs only during delivery.

Inspect the evidence and proposed action

Retrieval filters documents to active entries belonging to demo-a or public, then ranks lexical term overlap. For the refund question, the eligible hit is:

{
  "id": "billing-routing",
  "version": 1,
  "text": "Send refund requests to billing review."
}

The fixture proposes label billing and cites that document's exact version and text. Validation requires a closed output shape, an allowed label, a citation among the retrieved hits, an exact quotation and agreement with the policy document's routing label. An inactive or foreign-owner document cannot authorize the result merely because its ID appears in the response.

This proves a narrow structural relationship between supplied evidence and the proposed label. It does not prove that lexical retrieval found every relevant policy or that a language model understood the question. A missing output fixture becomes an abstention; an unsupported citation becomes a rejected work item. Neither creates an approval.

Before requesting approval, the worker stores the complete proposal, including the observed ticket version, creation time and expiry. Inspect prepared and trace in the inbox output to see those records. The proposal should be:

{
  "owner": "demo-a",
  "ticket_id": "T100",
  "label": "billing",
  "expected_version": 1
}

Approve, apply and retry in separate processes

Read the proposal, then run these commands within its 120-second lifetime. This exercise sets expires_at to now + 120 in capstone.py; it is a local example policy, not a universal approval timeout.

python3 capstone.py decide --db demo.sqlite --choice approved
python3 capstone.py apply --db demo.sqlite
python3 capstone.py apply --db demo.sqlite
python3 capstone.py inspect --db demo.sqlite

The first apply returns completed, replayed: false, billing and version 2. The next returns the stored outcome with replayed: true. Inspection still shows one receipt and version 2. Each command starts a new Python process, so the result comes from SQLite rather than an in-memory flag.

The completion receipt remains readable after expiry because that request retrieves a past result; it does not authorize another update. An approved proposal whose first apply arrives at expiry is refused. If you waited too long during the walkthrough, use a new database and repeat Prepare.

For rejection, prepare a different database and choose --choice rejected. A subsequent apply refuses with approval_not_active:rejected; the ticket stays unassigned. The command's nonzero exit and error are expected evidence of that refusal.

The inbox state review_created means processing handed off to an approval record. It is not the final action status. Read the approval state, receipt and audit history to distinguish pending, rejected, expired, stale and completed outcomes.

Test both recovery boundaries

Preparing the work and creating its approval are separate commits. The suite injects an exception after the approval commit but before marking the inbox handoff. A new worker process reads the same prepared bytes and expiry, checks the already-created approval and finishes the handoff. It does not silently rebuild the proposal using a newer target version or create another approval.

Applying the approved action updates the ticket, receipt and audit state in one SQLite transaction. The suite injects an exception after the ticket update. A new process sees the old label, version 1 and zero receipts. A later apply succeeds once. Python's transaction-control documentation explains the commit/rollback primitives used here.

These are controlled exceptions and fresh-process checks. They are not process-kill, power-loss or distributed exactly-once tests. Run one worker at a time. A remote CRM would require its own idempotency and reconciliation design; a local transaction cannot roll back a remote side effect.

Read every result, including refusals

ScenarioRequired observed outcome
Completed action, new process, retry after expirySame stored result; one receipt
Ticket version changes after approvalStale refusal; original label remains
First apply after expiryExpired refusal; zero receipts
Reviewer rejectsApply refuses; zero receipts
Apply before approvalApply refuses; zero receipts
No retrieved evidence and abstaining outputNo approval created
Citation names another owner's documentOutput rejected; no approval created
Event targets a different ticketHTTP 400 before inbox creation
Signature covers different bytesHTTP 401 before inbox creation
Same ID, different signed contentHTTP 409; original event still completes normally
Stop after approval creationNew worker resumes; one approval and one final receipt
Failure inside action transactionRollback visible in a new process; retry completes once

The suite also repeats each valid ingress delivery before processing and checks that the worker becomes idle after handing off or refusing the item. The report includes SHA-256 fingerprints for code and fixtures. Those hashes identify the tested inputs; they do not certify the application as secure.

Make one change and account for its effect

In a disposable copy, change the fixture citation ID from billing-routing to other-owner, keeping the other fields. Run Prepare against a new database. The delivery should still be accepted, but processing should return rejected with unsupported_label_or_citation. Inspection should show zero approval records and zero receipts. Restore the fixture afterward.

That exercise separates valid delivery from valid proposed work. A signed sender can still deliver a request whose resulting proposal should be refused.

Keep your final packet small enough to review: code and fixture hashes, the twelve-case report, one manually inspected approval, one changed-fixture result and a list of untested boundaries. If you later substitute a real model, retain this output contract and add task-quality evaluation, cost/timeout measurements and adversarial inputs. If you substitute an external service, test its action and recovery behavior separately.

The course ends with a reproducible workflow and evidence of its limits. The Agent Engineer career guide explains how to turn that work into an assessable portfolio. The course index lets you revisit the component that needs more practice.

Zarif

Zarif

Zarif builds AI agents and automation workflows and writes about what holds up in production: the sources worth following, the roles the AI era is creating, and agent workflows you can inspect end to end.