← Back

Building a Campaign Operations Agent

Jul 5, 2026 (2mo ago)

Every campaign starts the same way. Someone needs an email built.

What used to follow was a relay race. A request gets dropped in Slack. We open Linear and file a ticket. Our team opens Knak, builds the email, and works through drafts with stakeholders and approvers there. Then we export the HTML and rebuild it in Customer.io, our sending platform, by hand.

None of that is the work. The work is the campaign. The rest is logistics.

So we taught mOperator, our Slack-native marketing ops agent, to run the logistics. Now the whole thing happens in one thread.

mOperator is more than this one pipeline. Most of what it does day to day is the operational grind around campaigns: importing and cleaning lead lists, creating Salesforce campaigns, and standing up landing pages in Contentful or event pages in Luma. We shared a light version of that general-purpose agent publicly on GitHub, without the campaign build pipeline described here. Campaign builds are one more of its jobs, and this post is about that one.

We built this over an offsite in NYC, where three of us on the MOPs team spent two days wiring the pieces together. On the walk back to the hotel we passed one of our own Ship billboards, which turned out to summarize the week pretty well.

One of Vercel's Ship billboards in Manhattan at sunset: Build agents. Secure agents. Ship agents.

One of our Ship billboards in Manhattan. Build agents, secure agents, ship agents.

The relay race, before

Our MOPs team is three people, including Ethan, who owns campaign ops and email. We build every campaign email in Knak, work through drafts with stakeholders, and run each build through a dual-custody QA pass across the team before anything goes out. That process is solid. The problem was never the QA. It was everything around it.

The old path touched four tools and at least two people:

  1. Slack the request, usually a Notion doc.
  2. Linear a ticket, filed before any building starts, so the work is tracked.
  3. Knak our email builder. We translate the request into a built email, working through drafts with stakeholders and approvers.
  4. Customer.io the sending platform. We migrate the Knak email into Customer.io.

Every arrow between those tools is a copy and a paste, and every one of them takes time. The building and the QA are skilled work our team is genuinely good at. The shuttling of data from one system to the next is not the skilled part, and doing it by hand is just slow.

That is exactly the kind of work to hand to a machine.

The pipeline, after

Here is the whole flow now:

Slack form, deterministic parse, auto-build in Knak, review in Knak, one-click sync to Customer.io.

Slack form Parse Knak build Review in Knak Sync Customer.io

The build pipeline. The dashed steps are human: a teammate submits the form, and the requester reviews and approves the email in Knak. Everything else is mOperator, a deterministic parse, a Knak build on a lean theme, and a one-click sync that pushes the approved email into Customer.io through the API.

A teammate fills out a Slack workflow form. mOperator reads it and builds the email in Knak, then posts an "Open in Knak" button back to the thread. The requester reviews and edits it right there. When it is ready, one click syncs it into Customer.io through the API, and mOperator posts the "Open in Customer.io" link back to the same thread. Nobody on our team hand-builds anything.

The first time it ran end to end, a campaign request became a built, on-brand email in Knak and a draft in Customer.io with nobody assembling it by hand. A person still reviews it, edits in Knak if something is off, and approves. Building the email is just no longer the job.

Let me walk each stage.

The front door: a Slack form

The entry point is a Slack workflow, not a chat prompt. That is deliberate.

You can let people describe an email in free text and have an LLM sort it out. But campaign requests have a shape: audience, subject, preheader, body, CTA, brand, send date. A form captures that shape cleanly, and a form means the parse can be deterministic. No guessing, no hallucinated fields, no "did it understand me?"

The form also draws a line we always wanted and never had a good place to enforce. It expects approved copy.

In the old shape, a request often reached us before the copy was final. We would build the email, then edit through several rounds as it moved through the review chain. Our team absorbed a lot of that editing. It worked, but it put the drafting cycle downstream of the build, which is the wrong order. The new workflow moves that work upstream: drafting and stakeholder sign-off happen before the form gets filed. Marketers get a clear place to self-serve the copy, and the build starts from something final. mOperator reads the structured submission and turns it into a build request. The one place judgment is needed, cleaning up body copy while preserving bold and links, is handled in our own parser before anything is sent downstream, so what reaches the builder is predictable.

Rules for structure. AI only where judgment is actually required. That split shows up everywhere in this system.

Auto-building in Knak

Knak is where the email gets built. It also has an API, which is the part that made this possible.

There is one constraint worth knowing: Knak's API will not let you inject exact HTML. The only way to populate a real email body through the API is its AI generate endpoint. So that is what we drive. We hand it our cleaned content against a lean, on-brand theme, and it renders a built email.

A few things we learned the hard way:

  • Output is theme-driven, not prompt-driven. Telling the generator "don't add a testimonial section" does not reliably prune it. Structure comes from the theme, not from pleading in the prompt. The fix was a lean theme, not more prompting.
  • Naming is automatic. We generate names to a strict convention (region, type, brand, title, date, Linear ID) so every asset is consistent and searchable without anyone having to think about it.
  • Preserve the formatting yourself. Bold, links, and structure have to survive our parser, because the builder renders exactly what it is given.

When it is done, mOperator drops an "Open in Knak" button in the thread. The requester sees a real, built email in the tool of record, seconds after submitting a form. From there they review it in Knak and edit directly if something needs to change. Nobody starts from a blank canvas, and nobody hand-builds the email.

Syncing into Customer.io

Once the email passes review in Knak, syncing it into Customer.io is a single click. Knak fires a webhook, a small middleware fetches the finished asset, and the email shows up in Customer.io's Design Studio ready for a human to publish.

This is the part we rebuilt the most, and the interesting part is what we got wrong first.

Our first instinct was to make the Customer.io email fully editable: parse Knak's exported HTML into Customer.io's native Design Studio components, buttons, hero images, dividers, footers. We already had a renderer for exactly this, built for an earlier email-migration project, so the reuse felt free. It emits Customer.io's own component markup, an <x-base> / <x-section> shell wrapping native components, pushed through the Design Studio API:

<x-base>
  <x-section width="640px" :outer-background="{ light: `#FFFFFF`, dark: `#0A0A0A` }">
    <x-heading>Ship 2026 is coming to New York</x-heading>
    <x-text>Doors open <strong>June 30</strong>. Space is limited.</x-text>
    <x-image :src="{ light: `hero-light.png`, dark: `hero-dark.png` }" alt="Ship 2026" />
    <x-button href="https://vercel.com/ship?utm_source=customerio">Save your seat</x-button>
  </x-section>
</x-base>

In practice it broke things. Our templates shift in Knak during the build, and every gap between Knak's renderer and Customer.io's component set turned into a small visual regression: images dropped, padding collapsed, light and dark mode falling out of sync. Someone had to open each email in the Customer.io editor and hand-fix it, which defeats the entire point of the automation. The component model re-interprets the email, and re-interpreting by hand is exactly what we were trying to stop doing.

So last week we changed the approach. Instead of re-rendering into components, the sync now pushes Knak's own exported HTML straight into Customer.io, with the CSS inliner on so Knak's <style> block survives, and rewrites the tracking UTMs on the way through. What Knak renders is exactly what Customer.io sends.

Knak is the source of truth now. We can still edit in Customer.io if we need to, since it renders the HTML in a visual editor and lets you drop into the raw markup. But editing in two places is how two systems drift apart, so we keep it to one: copy is approved upstream, edits happen in Knak, and re-syncing through the integration pushes the change downstream.

What's automated, what stays human

The throughline in everything we build: rules for speed, AI for judgment, humans for authority. This pipeline draws the line on purpose.

Is this an agent? The build itself is mostly a workflow, and that is deliberate. mOperator is the agent: it reads a request and decides which tool to reach for. This pipeline is one of the deterministic rails it runs on. The autonomy is in choosing the job, not in improvising the email. For a system that touches real campaigns, that is exactly the tradeoff we want: predictable where it can be, judgment only where it has to be.

Automated:

  • Parsing the request
  • Building the email in Knak
  • Naming to convention (Linear ID read from the thread)
  • Pushing Knak's HTML into Customer.io
  • Setting subject, preheader, from/reply-to
  • Rewriting UTMs
  • Posting links back to the thread

Deliberately manual:

  • Review and any edits in Knak, by the requester, right after the build
  • Visual review of the built email, run as a dual-custody QA pass across our MOPs team
  • The final publish

A machine can assemble the email. It should not be the one who decides it is good enough to send. Two sets of eyes still sign off, and the last click stays with a person, on purpose.

The bigger picture

The point was never to build emails faster, though it does that. It was to remove the operational work around them.

The point is that our campaign stack, Slack, Knak, Linear, Customer.io, Salesforce, used to be a set of destinations a human walked between, carrying data by hand. Now it is an assembly line that runs from a single Slack thread. The tools did not change. What changed is that mOperator now connects them and carries the work between them, so a person does not have to.

The migration was the boring part, moving old emails to a new platform. This is the interesting part: the same building blocks, turned around to power how the team ships new work.

Fewer arrows between tools. Fewer manual handoffs. More time on the campaign, less on the logistics.


I'm part of Customer.io's referral program. If this got you curious and you want to try it, here's my referral link — using it may earn me a credit at no cost to you.