English
English
English
English
A field-marketing team runs a sales operation — agents work scheduled shifts (typically going door to door, sometimes at public events) and sign up new customers. The team wants SalesDash to show, per agent, how many shifts they've worked, how many sales they've made, and the productivity that falls out of the ratio — sales per shift.
Two distinct things to record, two source models to read them from, two processors. Unlike the GoHighLevel example (where two processors share work on related source models on the same provider) or the retail example (one processor doing everything), this integration's defining shape is two independent processors on two different source-model types, both writing activities attributed to the same agent.
The activities the spec writes feed metrics roughly like:
"Shift" activities."Sale | accepted" and "Sale | cancelled" into one metric."Sale | accepted" only."Sale | cancelled" over gross sales. (The cancelled-count metric does need to exist on its own to feed this ratio; it just isn't usually displayed anywhere standalone.)One provider — the platform the team uses to run their operation — feeds two sources:
Both processors handle their own agent identities — each event from the platform already includes the agent's name and email, so neither processor needs a separate user-only source like the GHL example. Each upsert uses the same external_id scheme (the platform's user ID), so both processors write to the same agent_external_identity records — once linked to a SalesDash agent, both kinds of activity show up under that agent.
Assumption to verify
The framing here assumes the team uses one platform for both sales tracking and shift planning. Some teams split these across separate systems. In that case there are two providers (one per system) instead of two sources on one provider, but the rest of the spec shape doesn't change.
The webhook payload from the platform is the source model — variable form — and contains the agent (form.user), the customer's submitted form, and metadata about the sale (status, value, etc.). No enrichments needed.
upsert_agent_external_identity with:
form.user.idform.user.nameform.user.emailif_else deciding the activity type. A define_variable named activity_type is set by an if_else whose condition is an equals_one_off check of form.status against the haystack of "cancelled" / "dropped". When the status matches one of those, the then branch sets activity_type to "Sale | cancelled"; the else branch sets it to "Sale | accepted".
This pattern again
Encoding a record's state in the activity-type — "Sale | accepted" vs "Sale | cancelled" — is the same idea as the [0 euro] pattern in the retail example. Different predicate (state instead of value), same structural pattern: separate types so downstream metrics can count each independently or count them together.
upsert_activity for the sale: "sale|" joined with form.id via concat.activity_type variable from step 2.form.createdAt wrapped in parse_datetime.form.amount (or whatever your platform calls the sale value). Sum-style metrics aggregate over this for revenue reporting.The shift-source payload is the source model — variable item — and contains the agent (item.user), the date, the type of shift, and a couple of validation flags. No enrichments needed.
upsert_agent_external_identity with:
item.user.id. Same scheme as the Sale Processor's step 1 — both processors write to the same identity for each agent.item.user.nameitem.user.emaildefine_variable named activity_type, set to the literal "Shift: " joined with item.type via concat. Example results: "Shift: Morning", "Shift: Evening" — depends on what shift types the platform defines.
if_else that adds an [unvalidated 0-score] suffix to the activity type when the shift didn't produce anything and hasn't been reviewed:
and of two not checks: that item.hasSales is not true, and that item.isValidated is not true.activity_type via define_variable, joining the existing value with the literal "[unvalidated 0-score]" via implode_with_spaces.The result: a shift with at least one sale OR a shift that's been reviewed gets the plain "Shift: <type>" activity type. A shift with no sales and no review gets "Shift: <type> [unvalidated 0-score]" — the team's metrics use this suffix to exclude unreviewed empty shifts from productivity calculations until someone has had a chance to look at them.
upsert_activity for the shift:
item.date joined with the literal "|" and item.user.id via concat. One activity per (date, agent) pair — if the same shift is re-polled, it overwrites the same record rather than creating a new one.activity_type variable from steps 2–3.item.date wrapped in parse_datetime.Both processors write activities attributed to an agent — that's how every activity-producing processor works. The thing worth noting in this example is that both use the same external_id scheme for agents (the platform's user ID), so an agent who has worked a shift and made a sale ends up with one identity record receiving activities from both processors. Metrics like productivity (sales per shift) then work as soon as both kinds of activity exist.
The two source models don't talk to each other. There's no "this sale happened during this shift" linkage in the spec — just two activity streams sharing an agent. If you wanted to attribute specific sales to specific shifts, you'd configure a shared contact_key between them and define metrics around that — not something this pair of processors does on its own.
When adapting this for your own integration:
No separate platform for shifts. Some operations don't track shifts at all — they treat each sale as its own implicit "effort unit." In that case skip the Shift Processor entirely and let sale count be both the input and the output (productivity becomes a different shape — sales per active day, say, derived from the activity moments).
More states than just accepted/cancelled. If your platform's status field has many possible values (pending, declined, refunded, …), don't extend the if_else into a long chain. Build the activity_type directly by joining the literal "Sale | " with form.status via concat. Each distinct status produces its own activity type ("Sale | pending", "Sale | declined", "Sale | refunded", …), and the team configures which types to count where in the metric definitions — not in the spec. The advantage: changing how states map to metrics is a metric-config change, done in minutes. Hardcoding state mappings in the spec means re-running the spec against every source record whenever the mapping changes — often slow, sometimes impossible depending on the integration.
Shift hours instead of shift count. If you want to measure shift hours rather than shift occurrences — to compare a four-hour Saturday shift to a one-hour Tuesday slot fairly — populate the activity's value field with the shift duration and configure metrics to sum on it rather than count.
Cancellations on the shift side. If a shift can be cancelled (the agent didn't show up), add a [cancelled] suffix on the Shift Processor mirroring how cancellations are handled on the Sale Processor.