Deduplication & Outcomes
Two data-plane concepts that shape routing decisions — inbound duplicate suppression, and the outcome feedback loop that closes on learning and billing.
Two trust-plane concepts sit on either side of a Route, guarding the quality of the work that clears. Deduplication runs before routing to suppress duplicate inbound Work, so a buyer is never charged twice for the same lead. Outcomes are recorded after routing to feed learning, attribution, and — the settled stage — billing. This page covers both.
Deduplication
The whole mechanism is a key, a window, and a branch. What matters is that a
duplicate is not a single outcome — the same hit takes one of three very
different paths, and only reject stops the routable from clearing:
Deduplication answers a narrow question at intake: have we already seen this routable? It computes a stable key from a routable's attributes, checks a time-windowed index scoped to a pool, tenant, or globally, and applies an action when a duplicate is found.
Deduplication is not idempotency. Dedup is inbound duplicate suppression — it drops or merges routables that look like ones seen recently. Idempotency is about exactly-once state progress under retries and crashes for work already accepted. They solve different problems; use the dedup key to avoid processing the same lead twice, and idempotency records to make a single accepted lead's side effects effectively-once.
The key
The dedup key is produced by a CEL expression evaluated against the routable.
The engine ships normalization helpers — hash(...), normalize_email(...),
normalize_phone(...), lower(...), trim(...) — so a key can canonicalize
before fingerprinting (for example, hashing a normalized email and phone
together). The config surfaces this as key_fields, the routable field
references used to compute the key; the default is routable.idempotency_key.
Scope and action
- Scope bounds where a duplicate counts:
pool,tenant, orglobal. When scope ispool, apool_idis required. - Action decides what happens on a hit:
Prop
Type
reject, merge, and flag are the accepted action values in the runtime.
reject is the default. Any other value (including allow) is not a valid
action and is refused by config validation.
Endpoints
All under /api/dedup.
Prop
Type
The check versus lookup distinction matters: check is the enforcement call
that records the access, while lookup is a passive read for inspection.
curl -X POST http://localhost:8080/api/dedup/check \
-H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
-H "Content-Type: application/json" \
-d '{ "key": "hash:abc123", "scope": "pool", "pool_id": "'$POOL_ID'" }'Config carries a ttl (an entry's time-to-live, e.g. "3600s"), so the index is
a time window, not a permanent ledger — a routable seen long enough ago is no
longer a duplicate.
Outcomes
An outcome is what happened after a Route — a contact, a conversion, a rejection. Outcomes are the feedback loop: they feed the learning strategies that adapt routing, and they carry the value and attribution that the settled stage uses for billing. An outcome of not converted inside the Warranty window is also what triggers a claw-back.
Learning strategies consume outcomes as their reward signal — a bandit updates its arms from recorded conversions and values. This page is the outcome record and its API; that page is the learning runtime.
A single decision can accrue multiple outcomes over time (e.g. contacted then converted). Each outcome is tied to a decision and, through it, to a pool and recipient.
Recording
POST /api/decisions/{decision_id}/outcomes records an outcome against a routing
decision.
Prop
Type
curl -X POST http://localhost:8080/api/decisions/$DECISION_ID/outcomes \
-H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
-H "Content-Type: application/json" \
-d '{ "outcome_type": "converted", "value": 1250.00, "currency": "USD", "source": "crm" }'Querying
Prop
Type
Stats endpoints are the reporting surface behind conversion dashboards and the per-pool and per-recipient rollups that inform both routing adaptation and billing attribution.
Related
Idempotency & Exactly-Once
What Ductor guarantees under retries and crashes — exactly-once state progress, at-least-once side effects, and how to collapse the gap.
Events & Event Sourcing
Ductor's three distinct event systems — aggregate event sourcing, the append-only bus, and the workflow transition journal.