Part 20 of 40 · 8 min read
One customer journey across the loops
Experimental · Work in progress · Internal R&D
Trace an enquiry through scope, delivery, billing and support with durable event identities, an outbox and explicit recovery.
Arunank Sharan · Kuzushi Labs ·
In this article
The real test of this platform is not whether each loop can finish a fixture. It is whether one customer commitment remains coherent as it moves through sales, delivery, support and finance. The handoffs are where duplicate work, missing context and inconsistent definitions usually appear.
The prototype already has useful cross-loop demonstrations: a lead becomes an opportunity, a won opportunity informs campaign economics, and delivery and purchasing events can feed financial records on a shared runtime. These examples are driven explicitly in local tests. Durable subscriptions and automatic recovery are the next layer.
Use a single running example
A customer requests the studio's document-search pilot. The founder approves a S$6,000 scope. Delivery requires a document sample and agreed evaluation questions. A S$2,000 deposit is received. After the accepted handover, the remaining amount becomes due. A later support issue leads to a correction and a product-improvement proposal.
Every step should refer to the same business identity chain: source interaction, lead, opportunity, accepted commitment, work order, invoice and receipt. The support ticket and product idea can then point back to the affected commitment without copying its commercial terms into separate stores.
Events carry identifiers; consumers reload facts
A useful event envelope has a stable event ID, tenant, subject, type, time, schema version, correlation ID and causation ID. The subject identifies the record that changed. The consumer reloads the authorised current state or a specified version before proposing an action.
{
"specversion": "1.0",
"id": "evt-017-delivery-accepted",
"source": "delivery",
"type": "delivery.accepted.v1",
"subject": "work-order-017",
"data": {
"tenant_id": "studio-demo",
"commitment_id": "commitment-017",
"accepted_version": 3,
"correlation_id": "customer-journey-017",
"causation_id": "review-017"
}
}
This is the proposed public contract shape. The platform already has CloudEvents support, but not every loop currently emits a complete payload of this form. In particular, some creation events need to use the newly created object ID rather than the empty target ID of the incoming request.
Exactly-once business effects require a transaction design
I would use a transactional outbox for the business mutation and its event, plus a durable inbox per consumer. A minimal relational design is:
CREATE TABLE consumed_event (
tenant_id TEXT NOT NULL,
consumer TEXT NOT NULL,
event_id TEXT NOT NULL,
consumed_at TEXT NOT NULL,
PRIMARY KEY (tenant_id, consumer, event_id)
);
Within one database transaction, the consumer claims the event, performs the permitted local mutation and writes its outgoing event. If the transaction aborts, all three roll back. A unique event claim prevents a retry from repeating the local effect.
That still does not make an external email or payment part of the database transaction. External effects need a durable operation record, provider idempotency where available, and reconciliation for the crash-after-success window. The local prototype's result stores and outboxes demonstrate pieces of this approach; they are not a proof of exactly-once effects across all systems.
Give each fact one owner
Sales owns commercial commitments. Delivery owns fulfilment evidence. Support owns issue state. Finance owns financial projections and reconciliation. The shared runtime lets them refer to each other; it does not mean every loop can edit every field.
For example, a support agent can propose a credit because an agreed feature failed. It should not alter the accepted scope to make the failure disappear. A planning agent can propose a new delivery date; the commercial effect of that change needs the appropriate workflow.
This ownership map is also how I keep the 14 walkthroughs from turning into 14 overlapping sources of truth. Campaigns remain inside Market to Lead; collections and renewals remain attached to their commercial owners.
Handle partial failure as a normal state
Suppose the quote is approved but the email provider times out. The opportunity should not revert to unapproved, and the system should not confidently mark the quote delivered. The send operation remains uncertain while reconciliation checks the provider.
Suppose delivery creates an invoice request, but the accounting connector is unavailable. The accepted delivery remains valid. A pending financial handoff explains what is delayed. Once the connector recovers, it consumes the same operation rather than creating a new invoice intent.
A workflow that spans services needs explicit compensation decisions. Cancelling a draft quote can be an update. Reversing a settled financial effect requires a new business operation. “Undo” is not one universal tool.
Test the boundaries, not only the happy path
The first integration matrix should include duplicate delivery events, out-of-order payment updates, a stale quote approval, a worker restart between commit and acknowledgement, and two events that refer to the same financial obligation. It should also include missing context: an event arriving before its linked agreement is available.
The founder's view should follow the correlation ID and show each handoff as pending, confirmed, rejected or uncertain. Then the platform can answer a business question—“what is stopping this customer from reaching a completed pilot?”—without requiring the founder to reconstruct the journey from service logs.
Put the mutation and its outgoing event in one transaction
A common failure is to update the opportunity, crash before publishing the event and leave delivery unaware that work was accepted. The opposite ordering can publish an event for a mutation that later rolls back. The outbox pattern addresses that local boundary by storing the state change and outgoing event together. AWS transactional outbox guidance.
The public store implements the essential shape:
from company_lab.store import Store
store = Store()
store.create("studio", "work-17", "work_order", {"state": "IN_REVIEW"})
store.change(
"studio", "work-17", 1, {"state": "ACCEPTED"},
"delivery-accepted-17", "delivery.accepted",
)
row = store.db.execute(
"SELECT subject,version,event_type FROM outbox WHERE tenant=? AND event_id=?",
("studio", "delivery-accepted-17"),
).fetchone()
assert tuple(row) == ("work-17", 2, "delivery.accepted")
A relay can now publish committed events and mark its own progress. If the relay crashes after publishing but before recording progress, it may publish again. The consumer still needs deduplication. An outbox prevents lost local handoffs; it does not turn every downstream system into an exactly-once executor.
Distinguish transport identity from business identity
event_id identifies a notification. operation_id identifies an attempted effect. obligation_id identifies what the business owes or expects. correlation_id connects a broader journey. Reusing one UUID for all four usually creates ambiguity when retries, adjustments or multiple consumers appear.
For the accepted pilot, delivery may emit an acceptance event and a later evidence-correction event. Both concern one milestone. Billing should use the milestone obligation to decide whether an invoice already exists. Support may create several cases under the same customer journey, each with its own identity.
Order events by business rules, not arrival time
A payment confirmation can arrive before the accounting invoice projection. The consumer can retain it as pending allocation until the referenced obligation appears. It should not discard the payment as invalid merely because a local projection is delayed.
Similarly, an old status event can arrive after a newer one. The consumer should compare provider version, effective time or authoritative current state according to that provider's contract. Applying every event as “set status to whatever arrived last” can regress a completed operation into pending.
A practical pending-event record includes the missing prerequisite, retry time, deadline and last observed source version. After bounded retries, route it to an operator exception with the evidence already collected. Endless redelivery is not a recovery strategy.
Compensations are domain actions
If delivery cannot begin after a deposit was accepted, the remedy may be rescheduling, a revised scope or a reviewed refund. There is no generic rollback of the customer journey. The platform should create a compensation proposal that references the original commitment and completed effects.
A compensation itself has an approval, operation identity and receipt. Replaying a failed workflow should not repeat a refund that already completed. The record should show which parts of the original work remain valid and which obligations were replaced or reversed.
Use leases for workers and receipts for effects
A job lease prevents two workers from owning the same local task simultaneously for a defined interval. It cannot stop an expired worker's already-sent network request. The expanded kit therefore separates worker fencing from provider idempotency and reconciliation. The lease chapter develops that distinction in detail.
The exercise is to crash at three points: after the local transaction, after external acceptance and after receipt storage. The expected recovered states differ. The first republishes or consumes the same event; the second reconciles uncertainty; the third returns the existing receipt. A system that simply reruns the whole customer workflow from its first prompt is likely to repeat business effects.