English
English
English
English
A retail company sells its own products — outdoor goods, in this example — through a quote-to-order flow. Salespeople send quotes; some get signed by the customer and become orders. The team wants SalesDash to show, per salesperson, how many quotes went out, what value those quotes carried, how many converted to deals, the total revenue from accepted deals, and a separate per-category value metric to vary what their slideshow leaderboards celebrate.
The integration is a single processor on a single source — different shape from the GoHighLevel pipeline tracking example. Each offer record from the source carries enough information that one spec can produce all the activities the team needs.
The activities the spec writes feed metrics roughly along the lines of:
"Quote" activities (with optional filtering on the [0 euro] suffix to exclude zero-value drafts)value on "Quote" activities"Deal | accepted" activities. (The basic spec doesn't track cancellations; if you add the cancellations variation below, this becomes the net sales metric, with a separate gross metric counting both "Deal | accepted" and "Deal | cancelled".)value on "Deal | accepted" activitiesvalue on the per-category aggregate activities (used on the slideshow for variety)One provider (a quoting system that exposes offers, the orders they become, and the line items on those orders) feeding one source feeding one processor. Each offer the source delivers carries everything the processor needs to produce up to four activities:
Unlike the GoHighLevel example, no separate user processor is needed — each offer record already includes the salesperson's name, so the same processor populates the agent identity from every offer it sees. (See Variations to consider below if your source doesn't include the salesperson's name on each offer.)
The source model is the offer record. It comes already enriched with its associated order (if any) and the lines on that order — so the spec sees offer.orders.0.lines directly, no further enrichment needed.
upsert_agent_external_identity with:
offer.employee.idoffer.employee.fullCompanyNameThat populates an agent_external_identity for the salesperson. Once the identity has been linked to a SalesDash agent, every activity below gets attributed to that agent.
An if_else sets quote_activity_type based on whether the quote has any value:
greater_than check of offer.totalPrice.inclVat against 0.define_variable that captures the literal "Quote" as quote_activity_type.define_variable that captures the literal "Quote [0 euro]" as quote_activity_type.This is a transferable pattern worth a callout:
The [0 euro] activity-type suffix
The team has a lot of zero-value quotes — drafts, placeholder offers, things that should be visible to the agent but shouldn't pollute revenue or count metrics. By writing those out as a separate activity type ("Quote [0 euro]" instead of plain "Quote"), the team can configure metrics to either include or exclude them:
"Quote" activities only counts the real-money ones."Quote" and "Quote [0 euro]" (by listing both types in the metric definition) counts all quotes."Quote [0 euro]" counts only the drafts.The pattern generalises: any time you have a record that's "the same kind of thing" but for which one binary state matters for metrics, put the state in the activity-type suffix and let metrics filter on it.
upsert_activity for the quote:
offer.id joined with the literal " | quote" via implode_with_spaces. The trailing "| quote" is what distinguishes this activity from the deal activity that the same offer might also produce — same offer, two different external_ids, two separate activities.quote_activity_type variable from step 2.offer.issuedAt wrapped in parse_datetime.offer.totalPrice.inclVat.offer.debtor.id. The customer's ID in the quoting system, used to group quotes by customer in metrics that count distinct contacts.An if_else wraps the rest of the spec. Its condition is an and of two checks: that offer.status.description equals the literal "Approved", and that offer.orders.0.orderType.id exists. The second check is there because some sources keep an empty orders array on quotes that haven't progressed yet — only when an actual order exists does it make sense to write a deal activity.
The else branch is empty. The then branch contains the steps below.
define_variable named order_moment, set to offer.orders.0.issuedAt wrapped in parse_datetime. Captured once so the next two upsert_activity steps can both use the same parsed value.
Same [0 euro] pattern as step 2, but on the order's charged amount: a nested if_else checks whether offer.orders.0.chargedAmount.inclVat is greater than 0, with the then branch capturing deal_activity_type as "Deal | accepted" and the else branch capturing it as "Deal | accepted [0 euro]".
upsert_activity for the accepted deal:
"order |" joined with offer.orders.0.id via implode_with_spaces. Different from the quote's external_id, so it's a separate activity.deal_activity_type variable from 4b.order_moment from 4a.offer.orders.0.chargedAmount.inclVat.offer.debtor.id. Same as the quote's, so metrics that group by customer treat the quote and its deal as related.The team wants a separate metric for one specific product category — outdoor kitchens — to surface alongside the headline revenue metric on the slideshow. The pattern: walk the order's lines, sum up the value of any line that belongs to the category, and emit a single aggregate activity carrying that total.
define_variable named outdoor_kitchen_total, initialised to literal "0".
for_each over offer.orders.0.lines, with item_var line:
Inside the loop body, an if_else checks whether line.article.group.id equals the outdoor-kitchen group ID in your source. When it does, the then branch updates outdoor_kitchen_total via define_variable — its new value is a sum of two things: the existing outdoor_kitchen_total, and a multiply of line.quantity and line.price.
After the for_each, another if_else checks whether outdoor_kitchen_total is greater_than 0. The then branch runs an upsert_activity for the aggregate:
offer.orders.0.id joined with the literal "| outdoor kitchen value" via implode_with_spaces."Outdoor kitchen value".order_moment from 4a.outdoor_kitchen_total.One activity per category, not per line
The for_each walks line by line, but only one activity gets written at the end — carrying the summed value of all lines in the category. The line items themselves don't become activities. This is a deliberate choice: the team's metric is "outdoor-kitchen value per agent per period," and one activity per order with the sum is the right shape for that.
If you wanted "number of orders with at least one outdoor-kitchen line" instead, the same activity also serves — just count the activities rather than sum their value. Same activity record, different metric configuration.
If you wanted "number of outdoor-kitchen line items" specifically, you'd put the line count in value instead of the summed monetary total — every metric on this activity then reads in line counts.
When adapting this for your own integration:
Cancellations. If your source signals when an accepted deal is cancelled (a status change to "Cancelled", say), add a third branch to step 4 — same external_id as the accepted-deal activity, type "Deal | cancelled" — so the same record gets overwritten with the cancelled type. Metrics counting "Deal | accepted" then naturally exclude cancellations; metrics configured to count both "Deal | accepted" and "Deal | cancelled" cover all closed deals.
More categories. The team here surfaces one category for slideshow variety. If you have several categories that matter independently (different commission tiers, different teams, different bonus structures), repeat the steps in 4d for each — a define_variable initialiser, a for_each aggregator, and a guarded upsert_activity per category. The pattern doesn't change, just the count of repetitions.
No quote step. Some retail flows go straight to order without a quote phase. In that case the source model would be the order itself (rather than the offer). Drop steps 2 and 3, and let step 4's activity be the primary record.
No salesperson name on the offer. If the offer record only carries the salesperson's ID, not their name, you'll need a separate user processor like the GoHighLevel example — otherwise the agent identity lands without a name, which makes it more difficult to recognise when linking.