Developer Stories10 min read

Rebuilding Workflow Automation: A Developer Story From 40 Zaps to OpenClaw

By Maya Chen

TL;DR

We migrated a launch critical marketing pipeline from 40 brittle Zaps to a single agentic backbone and recovered from a real incident that had sent duplicate emails and lost attribution. The rewrite focused on contracts, idempotency, retries, and human approvals, all modeled as stateful agents on OpenClaw. The result was a 99.6 percent success rate and a threefold improvement in P95 delivery time across channels, while keeping costs predictable. If you are planning serious workflow automation, this case study shows the design moves that matter most.

What broke and when

Two days before a seasonal campaign, a webhook storm hit our CRM after a partner flipped a setting. Our Zap chain fanned out into five tools, retried blindly, and created a perfect storm. Contacts received two emails, attribution rows went missing in the data warehouse, and Sales asked why fresh leads were blank. The chain looked clever on a whiteboard but it had no shared contract, no idempotency, and no real backpressure.

We had eight hours to stabilize. The quick fix was a kill switch that paused all non essential sends, then a batch repair script. The durable fix took two weeks. We rebuilt the path with agents on OpenClaw and treated each step as a state machine with clear inputs, outputs, and failure policy.

If you are new to our stack, ButterGrow is the hosted OpenClaw assistant that our team uses for production automation. You can skim the feature set to see the core AI marketing automation features and how they map to messaging, data sync, and approvals. When you are ready to kick the tires, you can get started in minutes.

The before state

The legacy path grew from experiments. It delivered value until it did not. Here is a simplified view of the old chain and its failure modes.

  • Trigger: form submit in a landing page service.
  • Transform: custom Zap that normalized UTM fields and enriched firmographics.
  • Fan out: email tool, CRM, spreadsheet backup, and warehouse loader.
  • Retry: each app retried on its own schedule without coordination.
  • Monitoring: ad hoc logs and a weekly spreadsheet tally.

Key problems we saw in logs and postmortems:

  • No single source of truth for event identity. Two apps computed contact keys differently.
  • No idempotency at the boundaries. Duplicate sends were possible whenever a retry raced with a slow 200 OK.
  • Retries lacked jitter and coordination. Hot spots got hotter.
  • Humans approved copy in Slack but the approval was not tied to the delivery that used it.

Before vs after at a glance

Measure Before (Zaps and scripts) After (agentic backbone)
End to end success 92.4% 99.6%
Median delivery time 2.7 min 1.1 min
P95 delivery time 11.8 min 3.1 min
Duplicate send rate 1.3% < 0.1%
Mean time to repair hours minutes

Numbers are from the first 30 days after cutover, measured with the analytics we describe later. If you want more on instrumentation, our separate write up on how to instrument agent behavior with analytics goes deeper.

The after state: an agentic pipeline on OpenClaw

We collapsed five tools into one orchestrated set of agents. The result is an agentic workflow that turns brittle steps into automated workflows with clear ownership. Each agent owns a small contract and a queue. Upstream signals become facts, not triggers. Downstream effects are idempotent and safe to retry. Human approvals are explicit and time bound.

  • Ingest agent. Accepts LeadCaptured events and validates them against a JSON Schema. Emits a canonical ContactUpsert command with a deterministic key.
  • CRM agent. Applies the upsert, records the external ID, and caches the mapping. If the CRM times out, it records a retryable failure with backoff and jitter.
  • Messaging agent. Generates a draft, requests sign off, and sends only with a valid approval token. The token binds the exact copy to the contact ID and expires in 30 minutes.
  • Warehouse agent. Writes immutable facts and is the system of record for analytics and replays.

We kept this mental model visible in runbooks and dashboards. When a queue backed up, we knew which contract was failing and why.

Contracts first

The first decision was to name the facts we care about and to write their shapes down. That sounds obvious, but our old chain never had a single, versioned schema. We fixed that by publishing an event contract repo and teaching the agents to reject messages that do not match.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "LeadCaptured",
  "type": "object",
  "required": ["eventId", "capturedAt", "contact", "utm"],
  "properties": {
    "eventId": { "type": "string", "pattern": "^[a-f0-9-]{36}$" },
    "capturedAt": { "type": "string", "format": "date-time" },
    "contact": {
      "type": "object",
      "required": ["email"],
      "properties": {
        "email": { "type": "string", "format": "email" },
        "firstName": { "type": "string" },
        "lastName": { "type": "string" }
      }
    },
    "utm": {
      "type": "object",
      "properties": {
        "source": { "type": "string" },
        "campaign": { "type": "string" },
        "medium": { "type": "string" }
      }
    }
  }
}

With a contract in place, we generated validators in code and returned cause codes for any rejection to make dashboards useful.

Idempotency everywhere

We borrowed an idea from payment APIs. Every external effect has a key that describes its intention. If a handler sees that key again, it returns the same result without doing the work twice. That single choice erases a class of flakey behavior.

// TypeScript. Idempotent email send with Redis as the key-store.
import { createClient } from "redis";
import { sendEmail } from "./email";

const redis = createClient({ url: process.env.REDIS_URL! });
await redis.connect();

export async function sendWelcomeEmail(cmd: {
  contactId: string;
  campaignId: string;
  templateId: string;
}) {
  const key = `email:${cmd.campaignId}:${cmd.contactId}:${cmd.templateId}`;
  const already = await redis.set(key, "1", { NX: true, EX: 60 * 60 * 24 * 7 });
  if (already === null) {
    return { status: "duplicate", key };
  }
  try {
    const res = await sendEmail(cmd);
    return { status: "sent", key, providerId: res.id };
  } catch (err) {
    await redis.del(key); // allow retry after failure
    throw err;
  }
}

For providers that support it, we also pass the key as an Idempotency-Key header. Stripe popularized the pattern and their docs helped us get the details right. See the reference at the end of this post if you want the primary source.

Retry policy that behaves

Our old stack retried in parallel and often made a small outage worse. We added exponential backoff with jitter, capped attempts, and a circuit breaker. The breaker flips open when a downstream crosses an error rate threshold and then allows small probes. This pattern keeps queues from amplifying a partial outage.

// Pseudocode with backoff and circuit breaker hooks
async function withRetry<T>(op: () => Promise<T>, opts: { attempts: number }) {
  let delay = 250; // ms
  for (let i = 1; i <= opts.attempts; i++) {
    try {
      return await op();
    } catch (e) {
      if (i === opts.attempts) throw e;
      const jitter = Math.floor(Math.random() * 100);
      await sleep(delay + jitter);
      delay = Math.min(delay * 2, 10_000);
    }
  }
}

We put the policy in one module so every agent could share the same behavior, and we logged the cause codes that explain why a message will be retried or dead lettered.

Approvals that bind content to delivery

Humans still pick the final copy for key campaigns. In the old flow, an approval in Slack did not bind to the exact payload that got sent. We fixed that by generating an approval token that encodes the contact ID, campaign ID, and a hash of the draft. The token expires quickly and is consumed by the sending agent.

If you run your approvals in Slack, our platform supports Slack approval workflows. That post covers the UI pattern and the five minute setup. We followed best practices for agent approvals in Slack so reviewers saw the diff and the expiry time.

Observability baked in

We shipped with traces, logs, and counters. The minimum we needed was a trace per contact, an event timeline, and counters for retries by cause code. We also wired a replay button that reads the last valid fact from the warehouse and requeues the command with the same idempotency key.

  • Traces show a waterfall for Ingest, CRM, Messaging, and Warehouse agents.
  • Counters group transient vs permanent failures so on call knows whether to wait or act.
  • Timelines help Sales see when a contact was enriched, emailed, and converted.

If you are starting from scratch and want a safe path, the answers to common questions on pricing, setup, and data handling can save a lot of time.

Step 1Model events and contracts

We wrote the event and command schemas first, then generated validators and TypeScript types. That gave us compile time confidence and runtime clarity. It also made it trivial to do a shadow run that compared the old outputs with the new ones.

Long tail query we validated with this step: how to migrate Zapier to OpenClaw without breaking existing attribution.

Step 2Make every effect idempotent

We started with email sends and CRM writes, then moved to comment inserts and warehouse loads. When we could not get a provider to behave, we put the key in a cache and treated the provider as a pure function from the perspective of our agents.

Long tail we tested here: idempotent email send pipeline with agents using a deterministic key.

Step 3Add retries with jitter and a breaker

We had one partner API that timed out randomly. The breaker kept pressure off the endpoint while probes kept checking if it had recovered. The jitter kept us from stampeding a cold cache. External references at the end of this post outline both patterns in more depth.

Step 4Bind human approvals to payloads

We replaced loose Slack approvals with signed tokens. The token tied the exact draft to an identity and an expiry. The send step refused to proceed without a valid token. Reviewers loved that they could see the diff and that nothing could ship unless they approved the exact copy.

Step 5Ship with instrumentation and a kill switch

We deployed with a per account feature flag and a kill switch that halted new sends while letting in flight work drain. We also shipped dashboards before enabling the new path. If you want a quick overview of platform capabilities and fit, the overview of what ButterGrow does is a good primer.

Results and tradeoffs

The new pipeline did what we needed. It also came with costs we accepted.

  • Reliability improved to 99.6 percent success with faster tail times.
  • The queue footprint grew by 20 percent because we store more facts.
  • On call got simpler because dashboards answered the first three questions.
  • Engineers now spend more time on contracts and less time on glue code.

We also found limits. Some providers will never implement proper idempotency and you will have to simulate it. Some approval flows want complex branching that belongs in a separate review service. It is better to keep agents small and let them call a dedicated reviewer than to bake a full reviewer into the agent.

What we would do next

  • Add rate limit aware scheduling for the few APIs that meter by minute. That would prevent head of line blocking when a small account shares a pool with a large one.
  • Build a replay that can compare side by side outputs for a contact and highlight differences automatically.
  • Move human approvals into a shared service with better reporting and retention.

If this approach matches your needs, you can try the hosted OpenClaw assistant that powers ButterGrow. The setup takes minutes and comes with sensible defaults for queues, retries, and approvals. When you are ready, head to the onboarding flow and ship your first agent run today.

References

Frequently Asked Questions

How did you enforce idempotency across email sends and CRM writes?+

We assigned a deterministic key to each event, such as `campaignId:contactId:step`. Every handler performed a read before write against a Redis set and short circuited if the key already existed. For third party APIs that support it, we also sent the key as an `Idempotency-Key` header so the provider deduplicated on their side.

What metrics proved the new pipeline was more reliable?+

We tracked end to end success rate, median time to deliver, P95 time to deliver, duplicate send rate, and replay rate. Success rose from 92.4 percent to 99.6 percent, P95 fell from 11.8 minutes to 3.1 minutes, and duplicate sends dropped to under 0.1 percent.

How did human in the loop approvals fit the agent design?+

Drafts were produced by a writer agent and parked in a review queue. A Slack action opened an approval card with the diff and safety checks, which triggered a short lived token that allowed a publishing agent to proceed only if the reviewer clicked Approve within 30 minutes.

Which failure patterns did you guard against first?+

We implemented exponential backoff with jitter for transient errors, circuit breakers for flaky downstreams, and dead letter queues for unprocessable events. Each pattern was chosen to isolate blast radius and to make retries observable.

How would you roll this out if you have hundreds of accounts?+

Use a feature flag per account and a shadow run that writes only to a control datastore. Compare outputs for a week, then enable the agentic path for 10 percent of accounts per day while watching error budgets and saturation on your queues.

What tools did you use to debug stuck runs in production?+

We used agent level traces and logs with correlation IDs, plus dashboards that broke down retries by cause code. The combination made it easy to find hot spots like a single CRM endpoint timing out. We also kept the last 50 events for each contact to allow quick replays.

Ready to try ButterGrow?

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

Book a Demo