agentic examples

The Drip Campaign

The Drip Campaign: every product ships one - welcome on day 0, tips on day 3, the gentle upsell on day 7 - and every team ships the same three bugs with it: the DOUBLE SEND (cron fired twice; your users got two welcomes and one impression: amateurs), the GHOST MAIL (user unsubscribed on Tuesday, your Thursday job mailed them anyway, now it's a compliance ticket), and the COHORT SMEAR (users who signed up Wednesday get Monday's schedule). The cures are boring and absolute: an …

Scheduling & Concurrency Round 20 Kasper Timm Hansen exit 0

source on github

bundle exec ruby examples/drip_campaign.rb

a real captured run

THE DRIP CAMPAIGN (three emails, three classic bugs, three boring cures)

  the week's outbox (cron double-fired on day 3; bo and di unsubscribed):
    day 0  welcome  -> ana
    day 0  welcome  -> bo
    day 0  welcome  -> di
    day 1  welcome  -> cy
    day 3  tips     -> ana
    day 4  tips     -> cy
    day 7  upsell   -> ana

  the three cures, verified: the (user, step) LEDGER made the
  day-3 double-fire invisible (same tick ran twice; the outbox
  can't tell); unsubscribes were checked at SEND time, the last
  possible moment - di unsubscribed the morning of their tips day
  and got nothing, because a check at schedule time is a promise
  made too early; and cy's whole sequence shifted with their
  signup day (day 1 welcome, day 4 tips), because offsets belong
  to the user's clock, not the calendar's. none of this needed a
  marketing platform - it needed a ledger, a late check, and
  per-user arithmetic. the right thing should be the default
  thing; in this campaign, it's the only thing.

source

# frozen_string_literal: true

# The Drip Campaign: every product ships one - welcome on day 0,
# tips on day 3, the gentle upsell on day 7 - and every team ships
# the same three bugs with it: the DOUBLE SEND (cron fired twice;
# your users got two welcomes and one impression: amateurs), the
# GHOST MAIL (user unsubscribed on Tuesday, your Thursday job
# mailed them anyway, now it's a compliance ticket), and the
# COHORT SMEAR (users who signed up Wednesday get Monday's
# schedule). The cures are boring and absolute: an idempotency
# ledger keyed by (user, step), unsubscribes checked AT SEND TIME
# - the last possible moment - and every offset computed from the
# user's OWN signup day. Time here is simulated; the bugs are not.
#
#   bundle exec ruby examples/drip_campaign.rb
#
# Runs offline; exits 1 unless the outbox is exactly right after a
# week that includes a double-fired cron and two unsubscribes.

require class="s">"bundler/setup"
require class="s">"agentic"
require class="s">"set"

Agentic.logger.level = class="y">:fatal

CAMPAIGN = [
  {step: class="s">"welcome", day_offset: 0},
  {step: class="s">"tips", day_offset: 3},
  {step: class="s">"upsell", day_offset: 7}
].freeze

USERS = {
  class="s">"ana" => {signed_up: 0},   # the loyal one: gets all three
  class="s">"bo" => {signed_up: 0},    # unsubscribes day 2: welcome only
  class="s">"cy" => {signed_up: 1},    # signs up a day late: schedule shifts with them
  class="s">"di" => {signed_up: 0}     # unsubscribes ON day 3, before the tick: no tips
}.freeze

OUTBOX = []
SENT = Set.new       # the idempotency ledger: (user, step), forever
UNSUBSCRIBED = {}    # user => day it happened

# One scheduler tick: compute due sends, deliver them as a plan
def tick(day)
  due = USERS.flat_map { |user, info|
    CAMPAIGN.select { |step| info[class="y">:signed_up] + step[class="y">:day_offset] == day }.map { |step| [user, step] }
  }
  return if due.empty?
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4)
  due.each do |user, step|
    task = Agentic:class="y">:Task.new(description: class="s">"#{step[class="y">:step]} -> #{user}", agent_spec: {class="s">"name" => class="s">"mailer", class="s">"instructions" => class="s">"send"})
    orchestrator.add_task(task, agent: ->(_t) {
      key = [user, step[class="y">:step]]
      next class="y">:duplicate_suppressed if SENT.include?(key)         # cure one: the ledger
      next class="y">:unsubscribed if UNSUBSCRIBED.key?(user)            # cure two: checked at SEND time
      SENT << key
      OUTBOX << {day: day, to: user, mail: step[class="y">:step]}
      class="y">:sent
    })
  end
  orchestrator.execute_plan
end

puts class="s">"THE DRIP CAMPAIGN (three emails, three classic bugs, three boring cures)"
puts

8.times do |day|
  UNSUBSCRIBED[class="s">"bo"] = day if day == 2
  UNSUBSCRIBED[class="s">"di"] = day if day == 3 # unsubscribes the morning of their tips day
  tick(day)
  tick(day) if day == 3 # the cron fires twice on day 3. it always eventually does.
end

puts class="s">"  the week's outbox (cron double-fired on day 3; bo and di unsubscribed):"
OUTBOX.each { |m| puts format(class="s">"    day %d  %-8s -> %s", m[class="y">:day], m[class="y">:mail], m[class="y">:to]) }
puts

by_user = OUTBOX.group_by { |m| m[class="y">:to] }.transform_values { |ms| ms.map { |m| m[class="y">:mail] } }
failures = []
failures << class="s">"ana should get the full sequence" unless by_user[class="s">"ana"] == [class="s">"welcome", class="s">"tips", class="s">"upsell"]
failures << class="s">"bo was mailed after unsubscribing" unless by_user[class="s">"bo"] == [class="s">"welcome"]
failures << class="s">"cy's schedule didn't shift with their signup" unless OUTBOX.select { |m| m[class="y">:to] == class="s">"cy" }.map { |m| m[class="y">:day] } == [1, 4]
failures << class="s">"di's send-time unsubscribe was ignored" unless by_user[class="s">"di"] == [class="s">"welcome"]
failures << class="s">"the double-fired cron double-sent" unless OUTBOX.size == OUTBOX.uniq { |m| [m[class="y">:to], m[class="y">:mail]] }.size
failures << class="s">"outbox size wrong (#{OUTBOX.size})" unless OUTBOX.size == 7 # cy's upsell lands on day 8, beyond this week

puts class="s">"  the three cures, verified: the (user, step) LEDGER made the"
puts class="s">"  day-3 double-fire invisible (same tick ran twice; the outbox"
puts class="s">"  can't tell); unsubscribes were checked at SEND time, the last"
puts class="s">"  possible moment - di unsubscribed the morning of their tips day"
puts class="s">"  and got nothing, because a check at schedule time is a promise"
puts class="s">"  made too early; and cy's whole sequence shifted with their"
puts class="s">"  signup day (day 1 welcome, day 4 tips), because offsets belong"
puts class="s">"  to the user's clock, not the calendar's. none of this needed a"
puts class="s">"  marketing platform - it needed a ledger, a late check, and"
puts class="s">"  per-user arithmetic. the right thing should be the default"
puts class="s">"  thing; in this campaign, it's the only thing."
exit(failures.empty? ? 0 : 1)