Skip to content

Operations

Operations are the steps in a processor that actually get something done — register a person, write a sale, store a value for later. Whenever you want your processor to make something happen, you reach for an operation. The other kind of step is a block (if_else, for_each), which decides which operations run but doesn't do work itself.

Most operations let you name their result with return_as so you can refer back to it later in the same spec — for example, registering an agent identity once at the top of the spec and then handing that same agent to every activity, sale, or call you write further down.

Operation families

  • Identity upsertsupsert_agent_external_identity, upsert_team_external_identity, upsert_project_external_identity, upsert_segment_external_identity, upsert_proposition_external_identity
  • Data upsertsupsert_activity, upsert_sale, upsert_call, upsert_anonymous_activity, upsert_period
  • Contact follow-upupdate_contact_activities, update_contact_periods, insert_contact_period_change
  • Utilitydefine_variable, lock_record_field

You'll typically build a processor in roughly this order: tell SalesDash who and what is involved (the people, teams, projects), then record what happened (the activities, sales, calls). The two sections below follow that order.

Identity upserts

When you want SalesDash to know about someone or something in your external system — a salesperson, a team, a project, a product — you upsert an external identity for them. This is almost always the first thing a processor does, because every activity, sale, call, or period you write later needs to know whose it is and what it's part of.

If you're new to the underlying concept, see Core Concepts → External Identities.

Every identity upsert takes the same core fields:

external_idrequiredstring or number

The ID this entity has in the external system.

external_nameoptionalstring or number

A human-readable name. Defaults to external_id if left empty.

project_external_identityoptionalproject_external_identity

Available when the Projects feature is enabled. Scopes this identity to a project. Not present on upsert_project_external_identity itself — projects don't nest.

If you upsert the same identity again — same external system, same external_id, same identity kind — SalesDash updates the one that's already there instead of creating a duplicate. You can re-run a processor as often as you like without piling up copies.

Some identity kinds add their own fields on top of these. Those are listed below.

upsert_agent_external_identity

Use this when you want to register a person — a salesperson, a setter, an SDR, anyone whose work needs to show up in metrics — as an agent in SalesDash. Almost every processor produces one of these, because the activities, sales, calls, and periods you write later all need an agent to attribute themselves to.

Extra fields on top of the common ones:

emailoptionalstring

The agent's email. Stored on the identity so it shows in the unlinked-identities list — admins typically link by name and email, so populating both makes their job possible.

team_external_identity_idoptionalnumber

The numeric ID of an earlier-upserted team identity (typically <your_team_var>.id). Groups the agent under that team.

Capture the result with return_as so the rest of the spec can attribute work to this agent — that's how upsert_activity, upsert_sale, upsert_call, and upsert_period know who they belong to.

Example

A processor on ghl_user registers every GHL user as an agent identity:

  • external_idsource_model.id
  • external_namesource_model.firstName and source_model.lastName joined into one string with an implode_with_spaces expression
  • emailsource_model.email

That's the whole spec. There are no enrichments and no activities — this processor exists purely to register every GHL user as an agent_external_identity so an admin has something to recognise and link to a SalesDash agent.

upsert_team_external_identity

Use this when your external system already has a team structure you want SalesDash to mirror — a sales region, a department, a squad. Once a team identity exists, you can group agents under it by passing the team's .id to upsert_agent_external_identity.

No extra fields beyond the common ones.

upsert_project_external_identity

Use this when you want to organise agents, segments, and activities under a project umbrella in SalesDash. The Projects feature must be enabled in your tenant for this to have any effect.

Many processors only need one project — they upsert it at the top of the spec with a literal external_id like "Project" and use it as the parent for every other identity that follows.

No extra fields beyond the common ones. Projects don't nest, so the project_external_identity field isn't present on this operation.

upsert_segment_external_identity

Use this when you want to slice a project into smaller scopes — a marketing campaign, a lead source, a sales region. Requires the Segments feature.

No extra fields beyond the common ones. Capture the result with return_as and pass it to upsert_activity or upsert_anonymous_activity to attach activities to this segment.

upsert_proposition_external_identity

Use this when you're recording sales tied to a specific product or service. You'll only need this if you're using upsert_saleupsert_activity doesn't have a proposition concept.

No extra fields beyond the common ones. Capture the result with return_as and hand it to upsert_sale.

Data upserts

Once the identities you'll need exist, the data upserts are how you record what actually happened — a deal moved stage, a call took place, a customer became active. They all reference the identities you upserted earlier.

Like identity upserts, they're keyed on external_id: if you upsert the same record again, you update it instead of creating a duplicate. The way you compose external_id is what decides whether you end up with one record per source event, one per state, or one per (record × stage). See External IDs decide what gets overwritten for the full picture.

Capture the resulting record with return_as if you need it later in the same spec — typical cases are locking the original timestamp with lock_record_field so it doesn't change on later runs, or back-propagating a value with update_contact_activities.

upsert_activity

Reach for this for almost everything: pipeline stage changes, demos booked, applications submitted, dossier creations — anything event-shaped that's attributed to a person. It's the most-used operation in the spec language.

external_idrequiredstring or number

Composed to control cardinality. See External IDs decide what gets overwritten.

activity_typerequiredstring

The activity's type — drives which metrics aggregate it.

agent_external_identityrequiredagent_external_identity

Typically the result of an earlier upsert_agent_external_identity.

momentrequireddatetime

When the activity happened. Wrap a string field with parse_datetime.

project_external_identityoptionalproject_external_identity

Available when the Projects feature is enabled.

segment_external_identityoptionalsegment_external_identity

Available when the Segments feature is enabled.

valueoptionalnumber

Numeric amount, used by sum-style metrics.

contact_keyoptionalstring

Identifier grouping activities by contact.

metaoptionalkeyed expression list

Additional key/value metadata stored on the activity. Available to event-channel templates (e.g. WhatsApp messages).

Example

A processor on ghl_opportunity_stage_change writes one activity per (opportunity, pipeline stage) pair to preserve the full deal history:

  • external_id — joins event.ghl_opportunity.id and event.ghl_opportunity.ghl_pipeline_stage.id with an implode_with_spaces expression
  • activity_type — joins the literal "GHL Opportunity:" and event.ghl_opportunity.ghl_pipeline_stage.name
  • agent_external_identity — the result of an earlier upsert_agent_external_identity step
  • momentevent.ghl_opportunity.lastStageChangeAt wrapped in parse_datetime
  • valueevent.ghl_opportunity.monetaryValue
  • contact_keyevent.ghl_opportunity.id

Every time the opportunity moves to a new stage, a new activity is created with a fresh external_id; activities for previous stages are preserved.

upsert_sale

Use this when you're recording a sale tied to a specific product, and you want SalesDash to track commission. The GenericCallCenterMetrics feature has to be enabled. If neither propositions nor commission apply to your setup, use upsert_activity instead — it's more flexible and easier to migrate later.

external_idrequiredstring or number
agent_external_identityrequiredagent_external_identity
proposition_external_identityrequiredproposition_external_identity

The product or service being sold.

sold_atoptionaldatetime

When the sale closed.

cancelled_atoptionaldatetime

Set on a later run if the sale gets cancelled.

contract_valueoptionalnumber

Numeric sale value.

commissionoptionalnumber

Explicit commission amount. If left empty, SalesDash calculates commission from the proposition's commission rules.

contact_keyoptionalstring

upsert_call

Use this when you're integrating a phone system — a power dialer, a softphone, a VoIP platform — and call duration is part of what you want to measure. Requires the CallMetrics feature.

external_idrequiredstring or number
agent_external_identityrequiredagent_external_identity
called_atrequireddatetime

When the call took place.

project_external_identityoptionalproject_external_identity

Available when the Projects feature is enabled.

durationoptionalnumber

Call duration in seconds.

resultoptionalstring

Free-form result label, e.g. "answered", "voicemail".

contact_keyoptionalstring

Identifier grouping calls by contact.

upsert_anonymous_activity

Use this when you want to record something that doesn't belong to any one person — ad spend, website traffic, marketing budget. Same shape as upsert_activity, just with no agent involved. The numeric value is what you'll typically be aggregating in metrics.

external_idrequiredstring or number
activity_typerequiredstring
momentrequireddatetime
valuerequirednumber

Required here (it's optional on upsert_activity). The value is the metric.

project_external_identityoptionalproject_external_identity

Available when the Projects feature is enabled.

segment_external_identityoptionalsegment_external_identity

Available when the Segments feature is enabled.

contact_keyoptionalstring

Example

A Google Ad Spend processor turns each row of incoming spend data into one anonymous activity per (date, project, campaign):

  • external_id — joins the cleaned date, the project name, and the campaign ID with literal " | " separators using a concat expression
  • activity_type — the literal "Google Ad Spend"
  • moment — the cleaned date wrapped in parse_datetime
  • value — the spend amount
  • project_external_identity and segment_external_identity — from earlier upserts in the same spec

The spend is attributed to the campaign (as a segment) and to the project, but no agent — the spend isn't anyone's individual achievement.

upsert_period

Use this when you want to track something that's ongoing rather than a single event — a customer being active, a ticket being overdue, a candidate being on assignment. Periods are what you build "currently open" metrics on top of, instead of "happened recently."

external_idrequiredstring or number
period_typerequiredstring

The period's type — drives which metrics aggregate it (e.g. "Active customer", "Ticket overdue").

agent_external_identityrequiredagent_external_identity
startrequireddatetime

When the period began.

valueoptionalnumber

The numeric value of the period.

project_external_identityoptionalproject_external_identity

Available when the Projects feature is enabled.

endoptionaldatetime

When the period closed. Leave empty while the period is still open; fill it in on a later run when the underlying state ends.

contact_keyoptionalstring

The pattern: upsert the same period (same external_id) every run. Leave end empty while the state is open, then fill it in on the run where it ends. Metrics that count "currently open" check whether end is null at the moment they aggregate.

Contact follow-up

Sometimes the right update isn't to one record but to every record tied to a particular contact — a deal's value changes in the CRM, an assignment switches to a different salesperson, a customer's state transitions. The contact follow-up operations match by contact_key and update across the matching records in one step.

update_contact_activities

Use this when something on a contact has changed retroactively — the deal's value, the assigned agent — and you want every activity you've already written for that contact to reflect the new state. SalesDash finds every activity matching the contact_key and updates them in one go.

contact_keyrequiredstring or number

Matches existing activities to update.

agent_external_identityoptionalagent_external_identity

If supplied, every matching activity is reassigned to this agent.

valueoptionalnumber

The new value to set on every matching activity.

Example

The GHL opportunity stage-change processor runs update_contact_activities right after writing the new stage activity:

  • contact_keyactivity.contact_key (captured from the just-upserted activity, which uses the opportunity ID as its contact_key)
  • valueevent.ghl_opportunity.monetaryValue

If the deal's value changes in GHL, the next time the processor runs it overwrites the value on every activity tied to that opportunity — not just the most recent one. Sum-style metrics built on the deal's value stay accurate going back through the deal's full history.

update_contact_periods

The period counterpart to update_contact_activities. Use this when the value or assignee on an ongoing or past period should change retroactively — same matching behaviour, same effect, but on Period records instead of Activity records.

contact_keyrequiredstring or number

Matches existing periods to update.

agent_external_identityoptionalagent_external_identity

If supplied, every matching period is reassigned to this agent.

valueoptionalnumber

The new value to set on every matching period.

insert_contact_period_change

Use this when you want to model a contact's lifecycle as a sequence of periods — "active customer," "trial," "churned" — and your source system tells you about each transition as it happens. Each call records a state change at a specific moment, and SalesDash handles the period boundaries: closing the previous period at the change moment and starting a new one.

contact_keyrequiredstring or number

Identifies the contact whose period history is being updated.

period_typerequiredstring

The type the contact is transitioning into at this moment.

agent_external_identityrequiredagent_external_identity
change_momentrequireddatetime

When the state change took effect.

valuerequirednumber

The numeric value of the new period.

project_external_identityoptionalproject_external_identity

Available when the Projects feature is enabled.

external_idoptionalstring or number

Optional ID for the new period record. Most use cases don't need it — SalesDash generates the period without one.

Three things get handled automatically. If the contact's currently-active period already has the same period_type, it's updated in place rather than replaced. If change_moment lines up exactly with the start of an existing period, that period gets updated (useful when you're re-running a backfill). Otherwise the active period is closed at change_moment and a new period is inserted, ending whenever the next existing period starts — or open-ended if no later period exists.

Utility

Two operations that aren't about producing data — they're about wiring the spec itself. define_variable captures a value for reuse later, lock_record_field protects a field on an upserted record from being overwritten on later runs.

define_variable

Use this when you want to compute something once and reuse it — typically a string you'll need in several places, or a value built up incrementally through if_else branches. The expression in value is evaluated, and the result is captured under whatever name you set in return_as.

valuerequiredany

The expression to evaluate. Whatever it returns becomes the captured variable's value.

Example

The TapRaise application form processor decides the activity's type with a chain of if_else blocks, and each branch updates a define_variable named activity_type:

  1. First, it sets activity_type to "Application valid" as a default.
  2. If form.originalDonationInterval is "once", it overwrites activity_type with "Application invalid".
  3. Otherwise, if the donation got cancelled or dropped, it overwrites activity_type with "Application valid cancelled".

By the time the spec reaches its upsert_activity, the activity_type variable holds whichever string survived the chain — and that's what gets written to the activity.

lock_record_field

Use this when you want to protect a specific field on an upserted record from being overwritten on later runs. The most common case: keeping the first timestamp on an activity that may be re-upserted later. Without this, every re-run would overwrite the activity's moment with whatever the source's latest timestamp is.

recordrequiredrecord

The captured record to lock a field on. Typically a variable from an earlier upsert_activity, upsert_sale, upsert_call, or upsert_period step.

field_namerequiredstring

The name of the field to lock — for example, "moment". Once locked, that field on this record won't be touched by later upserts that target the same record.

Example

The GHL opportunity stage-change processor upserts the activity, then locks its moment:

upsert_activity → return_as: activity
lock_record_field → record: activity, field_name: "moment"

If the same opportunity moves through other stages later, the activity's external_id changes (it includes the stage ID), so each transition gets its own activity — but if the same stage transition is replayed for any reason, the locked moment keeps the original timestamp. Time-windowed metrics stay anchored to when the event actually happened, not when SalesDash last saw it.