Skip to main content

Contact Plans

A contact plan is the one place that says how a campaign reaches a record: which channels it uses, in what order, how many tries each gets, what ends the journey and what caps it. It replaces the combination of dialing rules, omnichannel steps and campaign attempt settings with five plain lists, and every decision the engine makes comes with a sentence that says why.

Contact plans are enabled per organization. A campaign keeps running on dialing rules and omnichannel steps until its plan is switched on from the campaign's Contact Plan tab.

How a contact plan works

  1. A record enters the plan on the first step it can use.
  2. After every attempt the engine looks at the outcome. If the step has its own outcome row for it, that decides; otherwise the campaign's Ends when list decides; otherwise the disposition's default status applies and the step decides whether to try again or move on.
  3. If the record continues, Limits are checked in order. A limit can stop the record, close the channel it just used, close the number, or move the record to another campaign.
  4. Organization Guardrails run last. They can only make a record wait or stop.
  5. The next step's trigger sets when the next contact happens, and any additional sends on the step are queued.

Steps

A step is one touch. Steps run in order; a record moves to the next step when the current one has used its tries or an outcome row says so.

  • Channel — Call, SMS, Email or WhatsApp. Calls are only available on Voice and Omnichannel campaigns.
  • Trigger — when the step fires:
    • After the previous step, with an offset. This is the old step delay: "15 minutes after the call".
    • After the record was added, with an offset and an optional time of day in the record's time zone: "11:00 the day after upload".
  • Tries and Retry every — how many attempts the step's own channel gets and how long to wait between them. When the retry interval is blank the campaign's retry interval applies.
  • Caller ID and Keep agent — on a call step, the caller ID to present and whether to keep the agent who answers.
  • Only if — a condition on the record's fields, such as preferred_channel is whatsapp. A step whose condition fails is skipped, and the trace says so. Attempt counts and previous outcomes are not conditions here; triggers, Ends when and outcomes cover those.
  • Sends — the step's own channel is its primary send. Additional sends fire alongside it on other channels at their own offset ("email and SMS 15 minutes after the call"). A send is skipped, not failed, when the record has no address for that channel: no mobile number for SMS, no email address for Email.
  • Outcomes — optional overrides for this step only: "when the outcome is X, try again in 4 hours", "next step", "skip to step 4", "done", "stop" or "move to campaign". A step with no outcome rows uses the campaign's Ends when list and its own retry settings.

Ends when

Campaign-level, in order, any channel: a disposition or a wrap-up code that ends the journey.

  • Done marks the record completed; Stop marks it failed.
  • Do not contact can also close the number, or every number on the record.
  • Move to campaign hands the record to another campaign after the journey ends.

Outcomes not listed here use the disposition's own default status, as before.


Limits

Campaign-level caps. Each row has a counter, a channel scope, a threshold, an optional condition and a consequence.

CounterScopeExample
Contacts on the recordany, calls, SMS, email, WhatsAppafter 5 calls when premium is 100 or more: stop
Contacts on one numberany, calls, SMS, email, WhatsAppafter 3 calls on one number: close the number
Time since the record was addedhours or days10 days after the record was added when the wrap-up is not Personal Call Back: stop

Consequences:

  • Stop — the record fails.
  • No more on this channel — the record keeps going on the other channels; a call cap no longer silently kills the messages scheduled after it.
  • Close the number — only for per-number limits.
  • Move to campaign.

The campaign's Max Attempts and Max Attempts per Number appear as the first two limits, any channel, no condition.


Guardrails

Organization-level, narrow on purpose, and they run last:

  • Caps — a limit that applies to every campaign on a contact plan.
  • Quiet hours — a schedule and a channel scope. A send that would land outside the schedule is moved to its next open moment. This also covers messages, which dialing schedule restrictions never did.
  • Do not contact — a disposition that stops the record and marks the number, or the whole record, as do not contact.

Guardrails can only make a record wait or stop. They never advance it or change its channel.


Try it

The Try it panel on the Contact Plan tab shows what happens to a record after an outcome, as sentences, one per layer: the outcome, the step, the limits, the guardrails and the sends. Describe a record (its fields, attempt counts, how long ago it was added, which step it is on) or pick an existing one, choose an outcome, and read the answer. The same sentences are stored on the record when the engine runs for real, so the panel and production cannot drift apart.

On a campaign that still runs dialing rules and omnichannel steps, Try it narrates what those do instead, by calling the same code production calls.


Converting a campaign

Conversion is assisted and reviewed per campaign. Nothing is written until you apply it, and applying never switches the plan on: enabling it is a separate step.

  1. Open the campaign's Contact Plan tab and choose Convert from rules and steps.
  2. Read the proposal. It lists the steps, the Ends when rows, the limits and the "also" settings the converter built, and where each came from:
    • "Outcome is X -> done or stop" rules become Ends when rows.
    • "Attempts is N or more -> stop" rules, with or without a custom field condition, become limits with the condition kept. Per-number attempt rules become per-number limits; "days since import" rules become age limits.
    • "Attempts is N or more and outcome is Neutral -> caller ID X, retry after T" rules split the voice step at attempt N: the step from N on carries caller ID X and retries every T. The outcome condition is dropped, because a caller ID is set before the call, when there is no outcome yet. This is why those rules never fired.
    • "Outcome is X -> retry after T" rules become the step's retry interval when every retrying outcome shares one time, and per-step outcome rows otherwise.
    • "No agent assigned -> assign the agent" becomes Keep the first agent who answers.
    • Omnichannel steps become plan steps in order. A step's delay becomes its own trigger offset (the old engine waited the delay of the step a record moved into). Custom field gates become the step's only-if condition. Gates on the outcome or the attempt count are dropped, because Ends when and the triggers cover them; the proposal says so.
  3. Check the Not converted list. Schedule restrictions, per-interval rate limits, send-to-campaign actions, jump conditions and rules that mix a custom field condition with anything other than a stop have no automatic equivalent. Each entry says why and what to set up by hand (quiet hours under Guardrails, a "move to campaign" on an exit, an only-if on a step).
  4. Read the parity check. It runs the old engine and the plan side by side on every combination of current step, outcome, attempt count and representative custom field values, and lists every combination where the two disagree on the status, the next channel or the next time. Zero differences means the plan does what the rules do today. A difference is not always a mistake: the dead caller ID rules and a limit whose ordering changed both show up here, and the sentences from both sides explain each one.
  5. Apply the proposal. If the campaign already has a plan you must choose to replace it.
  6. Enable the plan on the campaign settings once you are satisfied. Records that have no step yet are placed on the first step they can use.

The same conversion is available from the command line:

bin/rails contact_plan:convert_preview[CAMPAIGN_ID] # proposal and parity check, writes nothing
bin/rails contact_plan:parity[CAMPAIGN_ID] # parity check only
bin/rails contact_plan:convert[CAMPAIGN_ID] # apply; refuses on differences unless FORCE=1

Reading the parity check

Each difference lists the inputs (the step the record was on, the outcome, the attempt number, the custom field values, the record's age) and both answers:

  • Status — try again, done or stop.
  • Next channel — the channel of the next contact when the record continues.
  • Next time — when it happens, within a minute.

Typical causes:

  • A First Match rule that ends the record on an attempt count sat before the outcome rules, so a positive outcome on a late attempt still stopped the record. The plan lets the exit win. Decide which behaviour you want.
  • A step's retry interval differs from the campaign's. The old engine only ever used the campaign's interval between tries on a step; the plan uses the step's. Set the step's interval to match if you want the old timing.
  • A rule the converter could not express. Its effect is missing from the plan until you add it by hand.