Core Concepts

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:

no yes reject merge flag routable arrives CEL key, normalized and hashed seen in window? record key with TTL continue to routing action error, not routed update original duplicate absorbed tag metadata

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, or global. When scope is pool, a pool_id is 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.