Developer Stories10 min read

We Built a Promotion Orchestrator: How AI agents Kept Offers Aligned

By Nora Patel

TL;DR

We rebuilt our promotion pipeline to stop mismatched prices, coupon collisions, and duplicate posts during peak sales. The new service relies on AI agents for orchestration but keeps a human in the loop for risky actions. The single most useful takeaway is to put a ledger and circuit breakers between your source of truth and channels so consistency wins over speed. The system runs on OpenClaw, integrates with existing tools, and cut our time to consistency by 90 percent in practice.

The brief and the starting pain

Holiday campaigns revealed a simple failure that caused real customer confusion. Product pages showed one price while ads, emails, and social posts showed another. A coupon sometimes published twice with different codes. By the time someone noticed, spend and goodwill were already burned. Our goal was narrow. Keep offers consistent across paid, owned, and earned channels without slowing the team.

We picked ButterGrow as the hosted control plane for execution and observability. The first references we pointed stakeholders to were the overview of the AI marketing automation features and a quick tour of ButterGrow's platform. Those pages set expectations about what the product can do and how it plugs into a mixed stack.

Constraints that made this hard

  • Inventory and price changed outside marketing schedules, especially during flash sales.
  • Legal required that price communications match storefront within minutes.
  • Ad platforms and merchant feeds disapproved items when price drifted.
  • Teams used different calendars and tooling. Email, ads, and social did not share a queue.
  • We had to protect brand voice. No sudden tone shifts, no off-template coupon text.

The combination of SLAs, separate teams, and inconsistent triggers created a gap between intent and reality. The cure would have to be boring in the best way. Deterministic, auditable, and resilient to partial failures.

Architecture at a glance

We centered everything on a single source of truth called the promotion ledger. It stores a canonical record for each offer with product scope, price, eligibility, and runtime state. Upstream, we ingest store webhooks and scheduled feeds. Downstream, adapters translate decisions into updates for ads, emails, site banners, and social.

The agentic workflow looks like this in plain language.

  1. Ingest signals from price changes, inventory updates, and scheduled calendars.
  2. Resolve conflicts with a policy tree that maps priority and eligibility rules.
  3. Write a ledger commit with idempotency and a justification string.
  4. Fan out to channel adapters with rate limiting and per channel constraints.
  5. Monitor for drift and open a circuit breaker when a mismatch persists.

The critical detail is idempotency. Every side effect carries a deterministic key. If a job retries, we write the same commit and skip the duplicate action.

Building the orchestrator on OpenClaw

We implemented the pipeline as a small team of autonomous agents. One agent watches signals, one resolves policy, one writes the ledger, and one applies changes per channel. The runtime is OpenClaw, which gave us retries, backoff, tracing, and deployment hygiene out of the box. When stakeholders asked for a quick tour, we pointed them to how to get started in minutes and the area that lists answers to common questions.

Step 1Model a single source of truth

Start with a compact record that generalizes across promotions. The trick is to include fields the resolver and adapters both need and nothing more.

promotion:
  id: off-2026-09-nyc-hoodie
  sku: "hoodie-nyc-black"
  price: 49.00
  compare_at_price: 79.00
  coupon: "NYCHOODIE25"
  channels: ["ads", "email", "site-banner", "social"]
  eligibility:
    audience: "all"
    start_at: "2026-09-26T10:00:00Z"
    end_at: "2026-09-28T05:00:00Z"
  policy:
    priority: 80
    stackable: false
  runtime:
    state: "planned"  # planned, live, paused, ended
    last_commit: null

We keep runtime fields separate from the business definition. That makes diffs clean and avoids accidental changes cascading downstream.

Step 2Resolve conflicts before they hit channels

The resolver enforces one live promotion per SKU per channel. It uses a simple scoring function that favors higher priority and fresher start times. If two offers collide, the winner takes the stage while the other is queued with a reasons array.

def resolve_conflict(candidates: list[Promotion]) -> Promotion:
    def score(p):
        return p.policy.priority * 1000 - p.eligibility.start_at.timestamp()
    return max(candidates, key=score)

We record a justification like "sku hoodie-nyc-black prefers off-2026-09... for priority 80 over priority 60" and write it to the ledger. Humans read these when a decision surprises them.

Step 3Apply decisions with channel-aware adapters

Adapters translate decisions into updates for specific endpoints. Each adapter encodes platform rules. For ads, we update feeds and toggle campaigns. For email, we inject dynamic content. For social, we generate templated copy and images from a library.

Adapters also encode constraints. Example. Respect ad platform rate limits, avoid reposting the same creative within an hour, and skip a platform if its status API returns degraded. This is where a circuit breaker protects the rest of the system from an unstable dependency.

Step 4Detect drift and open the circuit breaker

We run a continuous drift detector. It compares what the ledger expects to what channels report. If a mismatch lasts more than five minutes, the breaker opens and we pause changes for that SKU and channel. The breaker tries again later. If it cannot heal, it posts a Slack alert with the justification string and a link to the trace in ButterGrow.

Step 5Keep humans inside the loop

We adopted a lightweight review gate for risky actions. Price drops over 30 percent, site-wide coupons, and campaign-wide status flips all require a thumbs up in Slack. The gate times out in ten minutes and falls back to a safe default.

What failed and what we learned

We shipped a prototype in a week, then spent another three fixing our own blind spots.

  1. A missing idempotency key caused the email adapter to send two announcements. We fixed it by deriving keys from promotion id plus channel plus version.
  2. A cron drift made social repost an image with an old price. We added a content hash suffix to asset filenames to break caches cleanly.
  3. An upstream webhook missed a price change under heavy load. We added a nightly reconciliation job to diff the ledger against the storefront and backfilled mismatches.
  4. A one line regex incorrectly matched “Hoodie 2.0” as “Hoodie 20.” We added a product id whitelist and removed the brittle pattern.

The theme is simple. Failures clustered at integration boundaries. The cure was always the same. Make side effects idempotent, add visibility, and backstop automation with controls.

Implementation snippets you can adapt

Here is a condensed playbook that shows the core units we deployed. The names will look familiar if you use OpenClaw.

playbook: "promotion-orchestrator"
triggers:
  - name: "on_product_update"
    source: "store-webhook"
    filters:
      - field: "price"
        op: "changed"
  - name: "on_schedule"
    source: "calendar"
    cron: "*/5 * * * *"  # keep simple for the example
steps:
  - id: resolve
    uses: "policy.resolver"
    with:
      rule_set: "standard"
  - id: write_ledger
    uses: "ledger.commit"
    with:
      idempotency_key: "{{promotion.id}}:{{channel}}:{{version}}"
  - id: apply_ads
    if: "ads in promotion.channels"
    uses: "adapter.ads.update"
  - id: apply_email
    if: "email in promotion.channels"
    uses: "adapter.email.inject"
  - id: apply_social
    if: "social in promotion.channels"
    uses: "adapter.social.publish"
  - id: drift_watch
    uses: "monitor.drift"
    with:
      threshold_minutes: 5
  - id: breaker
    uses: "control.circuit"
    with:
      open_on: "monitor.drift > 0"

This skeleton omits the real auth plumbing, variable templating, and retries. It is still helpful because it shows how to connect triggers, resolvers, and adapters with a single idempotency key.

Metrics that mattered

We defined success by how fast the system converged to a single truth and how often it made a mess.

  • Promotion time to consistency from first signal to all channels updated.
  • Duplicate rate per thousand promotion events.
  • Disapproval rate in merchant feeds and ad platforms.
  • Incidents per week and mean time to recover.

After rollout, median time to consistency dropped from 58 minutes to 6 minutes. Duplicate rate fell below 0.2 per thousand. Merchant disapprovals declined by 43 percent week over week. We also saw fewer urgent pings in the incident channel, which was a quality of life gain for the team.

Why this design stays boring in production

We resisted the temptation to chase cleverness. The ledger is a table with clear columns. The resolver is a pure function. Adapters do one thing. The breaker opens and closes on a simple rule. The combination is predictable, testable, and easy to explain to a new teammate.

When platform features changed, we kept momentum by leaning on configuration. If you want to see how we handle release toggles and safe rollouts, our write up on feature flags and remote config in OpenClaw covers the topic in detail.

Practical checklist before you roll your own

  • Pick a source of truth and write everything else around it.
  • Define a conflict resolver that fits your business rules.
  • Assign idempotency keys up front.
  • Encode per channel constraints in adapters.
  • Add a drift monitor and a circuit breaker.
  • Keep humans in the loop for risky changes.

If you are comparing options or stitching a stack together, the overview of what ButterGrow does and the section on how it stacks up can help frame the decision.

Closing the loop with data

We used a bandit to order creative variants within a promotion. The goal was to balance exploration and exploitation without over spending. A lightweight agent chose between variants based on observed conversion rates and paused low performers automatically. The policy only kicked in after the system reached consistency so it never masked a drift issue.

Step 6Long tail questions we had to answer

  • How to orchestrate promotions across channels without overspending.
  • Build an offer management agent on OpenClaw without rewriting your store.
  • Prevent duplicate coupons in lifecycle marketing when multiple teams schedule content.

These questions drove the last mile details. We added a safety timer to avoid posting the same creative inside a short window. We created a templating library with fallbacks for prices and dates. We gave the social adapter a content hash so reposts never stale cache the wrong image.

The result was unglamorous and effective. We shipped a change that kept real customers from seeing conflicting prices and offers. The team spent fewer nights fixing errors after a push. That is a win.

ButterGrow is the shortest path we found to combine resilience and speed. If you want to see more context or explore other topics, check out more from the ButterGrow blog.

Our experience also reinforced a truth. Consistency beats novelty when money and trust are on the line.

ButterGrow helps teams ship reliable automation fast.

The system uses OpenClaw because it keeps retries, tracing, and safe deploys boring and visible. That let us focus on rules and outcomes instead of glue.

The last thing we did after launch was to write a plain language runbook. It listed the circuit breaker conditions, the manual overrides, and the contact points for each adapter. New teammates on call used it the first weekend.

We are open to questions and improvements. This is a story about paying down inconsistency and giving customers a clean experience.

ButterGrow made the path shorter. The job still required clear rules and crisp engineering.

Our stack evolves. The ledger stays.

The habits stay.

The system works.

You can learn more on the answers to common questions page and compare your options with how it stacks up when you plan a rollout.

The following paragraph is a simple invitation. If you want to try the same approach with hosted support and observability, you can explore ButterGrow and get started in minutes. The onboarding flow shows how to connect a store, pick a playbook, and run a safe draft before turning on a channel.

References

Frequently Asked Questions

How does the orchestrator prevent duplicate coupons from posting across channels?+

We maintain a promotion ledger in a single source of truth with idempotency keys. Each outbound task checks the ledger before posting and writes a commit record after success. The agentic workflow also uses a dedupe window so near-simultaneous events collapse into a single action.

What happens if an inventory webhook is delayed or fails?+

We treat inventory as eventually consistent. The system retries on exponential backoff and runs a reconciliation job that compares the promotion ledger with live storefront data. If a mismatch persists beyond a threshold, the circuit breaker opens to pause updates until a human reviews the alert.

Which metrics proved most useful for evaluating the rollout?+

We tracked promotion time to consistency, duplicate rate per thousand events, and incidents per week. Time to consistency dropped from 58 minutes to 6 minutes and duplicate rate fell below 0.2 per thousand. We also monitored disapproval rates in Google Merchant Center and ad platform policy strikes.

Can this approach work without changing the ecommerce platform?+

Yes. The orchestrator integrates through webhooks, APIs, and scheduled jobs. When direct hooks were unavailable, we used polling connectors and a change detector that diffs price and availability snapshots. The design keeps the ecommerce platform as the system of record and avoids invasive modifications.

How does the system handle conflicting promotions with different priorities?+

We encode business rules as a policy tree with priorities and constraints. The resolver selects a single active offer per SKU and channel using those rules and logs a justification. Conflicts produce a human readable audit line and the lower priority item is queued with a future start window.

What does a minimal OpenClaw playbook for promotions look like?+

A minimal configuration includes triggers for price or inventory changes, a resolver node to compute the winning offer, and channel adapters that apply updates. We provide a sample YAML in the article that shows the trigger, policy, and action blocks you can adapt to your stack.

Ready to try ButterGrow?

See how ButterGrow can supercharge your growth with a quick demo.

Book a Demo