Skip to content

Consumer retail with quote-to-order flow

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.

What gets measured

The activities the spec writes feed metrics roughly along the lines of:

  • Quotes sent — count of "Quote" activities (with optional filtering on the [0 euro] suffix to exclude zero-value drafts)
  • Quote value — sum of value on "Quote" activities
  • Sales — count of "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".)
  • Revenue — sum of value on "Deal | accepted" activities
  • Conversion rate — sales / quotes sent
  • Outdoor-kitchen value — sum of value on the per-category aggregate activities (used on the slideshow for variety)

The integration shape

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:

  1. The quote itself.
  2. The accepted deal, when the offer has a signed order attached.
  3. A per-category aggregate activity (outdoor kitchens here), when the order's lines include items in that category.

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.)

Spec walkthrough

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.

1. Agent identity

upsert_agent_external_identity with:

  • external_idoffer.employee.id
  • external_nameoffer.employee.fullCompanyName

That 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.

2. Activity type for the quote

An if_else sets quote_activity_type based on whether the quote has any value:

  • condition — a greater_than check of offer.totalPrice.inclVat against 0.
  • then — a define_variable that captures the literal "Quote" as quote_activity_type.
  • else — a 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:

  • A metric counting "Quote" activities only counts the real-money ones.
  • A metric configured to count both "Quote" and "Quote [0 euro]" (by listing both types in the metric definition) counts all quotes.
  • A metric counting "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.

3. The quote activity

upsert_activity for the quote:

  • external_idoffer.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.
  • activity_type — the quote_activity_type variable from step 2.
  • agent_external_identity — from step 1.
  • momentoffer.issuedAt wrapped in parse_datetime.
  • valueoffer.totalPrice.inclVat.
  • contact_keyoffer.debtor.id. The customer's ID in the quoting system, used to group quotes by customer in metrics that count distinct contacts.

4. The deal activity (only when the offer has been accepted)

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.

4a. Order moment

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.

4b. Activity type for the deal

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]".

4c. The deal activity

upsert_activity for the accepted deal:

  • external_id — the literal "order |" joined with offer.orders.0.id via implode_with_spaces. Different from the quote's external_id, so it's a separate activity.
  • activity_type — the deal_activity_type variable from 4b.
  • agent_external_identity — from step 1.
  • momentorder_moment from 4a.
  • valueoffer.orders.0.chargedAmount.inclVat.
  • contact_keyoffer.debtor.id. Same as the quote's, so metrics that group by customer treat the quote and its deal as related.

4d. Per-category aggregate

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:

  • external_idoffer.orders.0.id joined with the literal "| outdoor kitchen value" via implode_with_spaces.
  • activity_type — literal "Outdoor kitchen value".
  • agent_external_identity — from step 1.
  • momentorder_moment from 4a.
  • valueoutdoor_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.

Variations to consider

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.