English
English
English
English
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.
upsert_agent_external_identity, upsert_team_external_identity, upsert_project_external_identity, upsert_segment_external_identity, upsert_proposition_external_identityupsert_activity, upsert_sale, upsert_call, upsert_anonymous_activity, upsert_periodupdate_contact_activities, update_contact_periods, insert_contact_period_changedefine_variable, lock_record_fieldYou'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.
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 numberThe ID this entity has in the external system.
external_nameoptionalstring or numberA human-readable name. Defaults to external_id if left empty.
project_external_identityoptionalproject_external_identityAvailable 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.
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:
emailoptionalstringThe 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_idoptionalnumberThe 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:
source_model.idsource_model.firstName and source_model.lastName joined into one string with an implode_with_spaces expressionsource_model.emailThat'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.
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.
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.
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.
Use this when you're recording sales tied to a specific product or service. You'll only need this if you're using upsert_sale — upsert_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.
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.
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 numberComposed to control cardinality. See External IDs decide what gets overwritten.
activity_typerequiredstringThe activity's type — drives which metrics aggregate it.
agent_external_identityrequiredagent_external_identityTypically the result of an earlier upsert_agent_external_identity.
momentrequireddatetimeWhen the activity happened. Wrap a string field with parse_datetime.
project_external_identityoptionalproject_external_identityAvailable when the Projects feature is enabled.
segment_external_identityoptionalsegment_external_identityAvailable when the Segments feature is enabled.
valueoptionalnumberNumeric amount, used by sum-style metrics.
contact_keyoptionalstringIdentifier grouping activities by contact.
metaoptionalkeyed expression listAdditional 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:
event.ghl_opportunity.id and event.ghl_opportunity.ghl_pipeline_stage.id with an implode_with_spaces expression"GHL Opportunity:" and event.ghl_opportunity.ghl_pipeline_stage.nameupsert_agent_external_identity stepevent.ghl_opportunity.lastStageChangeAt wrapped in parse_datetimeevent.ghl_opportunity.monetaryValueevent.ghl_opportunity.idEvery time the opportunity moves to a new stage, a new activity is created with a fresh external_id; activities for previous stages are preserved.
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 numberagent_external_identityrequiredagent_external_identityproposition_external_identityrequiredproposition_external_identityThe product or service being sold.
sold_atoptionaldatetimeWhen the sale closed.
cancelled_atoptionaldatetimeSet on a later run if the sale gets cancelled.
contract_valueoptionalnumberNumeric sale value.
commissionoptionalnumberExplicit commission amount. If left empty, SalesDash calculates commission from the proposition's commission rules.
contact_keyoptionalstringUse 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 numberagent_external_identityrequiredagent_external_identitycalled_atrequireddatetimeWhen the call took place.
project_external_identityoptionalproject_external_identityAvailable when the Projects feature is enabled.
durationoptionalnumberCall duration in seconds.
resultoptionalstringFree-form result label, e.g. "answered", "voicemail".
contact_keyoptionalstringIdentifier grouping calls by contact.
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 numberactivity_typerequiredstringmomentrequireddatetimevaluerequirednumberRequired here (it's optional on upsert_activity). The value is the metric.
project_external_identityoptionalproject_external_identityAvailable when the Projects feature is enabled.
segment_external_identityoptionalsegment_external_identityAvailable when the Segments feature is enabled.
contact_keyoptionalstringExample
A Google Ad Spend processor turns each row of incoming spend data into one anonymous activity per (date, project, campaign):
" | " separators using a concat expression"Google Ad Spend"parse_datetimeThe spend is attributed to the campaign (as a segment) and to the project, but no agent — the spend isn't anyone's individual achievement.
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 numberperiod_typerequiredstringThe period's type — drives which metrics aggregate it (e.g. "Active customer", "Ticket overdue").
agent_external_identityrequiredagent_external_identitystartrequireddatetimeWhen the period began.
valueoptionalnumberThe numeric value of the period.
project_external_identityoptionalproject_external_identityAvailable when the Projects feature is enabled.
endoptionaldatetimeWhen the period closed. Leave empty while the period is still open; fill it in on a later run when the underlying state ends.
contact_keyoptionalstringThe 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.
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.
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 numberMatches existing activities to update.
agent_external_identityoptionalagent_external_identityIf supplied, every matching activity is reassigned to this agent.
valueoptionalnumberThe 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:
activity.contact_key (captured from the just-upserted activity, which uses the opportunity ID as its contact_key)event.ghl_opportunity.monetaryValueIf 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.
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 numberMatches existing periods to update.
agent_external_identityoptionalagent_external_identityIf supplied, every matching period is reassigned to this agent.
valueoptionalnumberThe new value to set on every matching period.
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 numberIdentifies the contact whose period history is being updated.
period_typerequiredstringThe type the contact is transitioning into at this moment.
agent_external_identityrequiredagent_external_identitychange_momentrequireddatetimeWhen the state change took effect.
valuerequirednumberThe numeric value of the new period.
project_external_identityoptionalproject_external_identityAvailable when the Projects feature is enabled.
external_idoptionalstring or numberOptional 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.
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.
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.
valuerequiredanyThe 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:
activity_type to "Application valid" as a default.form.originalDonationInterval is "once", it overwrites activity_type with "Application invalid".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.
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.
recordrequiredrecordThe captured record to lock a field on. Typically a variable from an earlier upsert_activity, upsert_sale, upsert_call, or upsert_period step.
field_namerequiredstringThe 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.