All jobs

Report Email Campaign Performance From Klaviyo

Yes. An agent reads Klaviyo campaigns from the last 90 days and writes campaigns.md ranked by revenue per recipient.

What the agent does

There is no repository on this run. The account is the Klaviyo connection: Rush's connector catalog lists Klaviyo as an official MCP server at https://mcp.klaviyo.com/mcp, and the catalog row says the account needs an Owner, Admin or Manager role. You connect it once with POST /me/connections, type mcp, that URL, and auth mode oauth, open the authorize_url while signed in to the console as the key's creator, and wait until GET /me/connections shows status connected. The session body then names that connection. A key run receives only the connections it names, so this request cannot see any other account you have connected, and the token never comes back in an API answer.

Klaviyo's tool list marks get_campaigns, get_campaign, get_campaign_message and get_campaign_report as read-only and available on the remote server. The agent calls get_campaigns, keeps sends whose send time falls in the last 90 days, and drops drafts that never went out. For each remaining campaign it calls get_campaign for the audience (a list or a segment) and the send time, get_campaign_message for the subject line, and get_campaign_report for performance. The remote server can be pointed at with the read-only query flag Klaviyo documents, which leaves send_campaign, update_campaign and delete_campaign unloaded. The prompt forbids those writes even if the connection loaded the full set.

Ranking is revenue per recipient, using only statistics the report payload actually contains. Open rate, click rate and unsubscribe rate are copied from that same payload. When a statistic is missing, the row says it was not returned and the agent does not invent a zero. Campaigns with no recipients stay in the file, below the ranked list, so a send that failed to estimate an audience is visible instead of disappearing into a divide-by-zero. The top five are then compared on three fields the tools did return: subject line, send time, and whether the audience was a list from get_lists or a segment from get_segments.

The file is campaigns.md. It opens with the window (the 90 days ending at the run), the account name from get_account_details, and how many campaigns were sent versus skipped as unsent. Then a table: campaign name, subject, send time, audience, recipients, open rate, click rate, unsubscribe rate, revenue per recipient. A short section after the table states what the top five share, and only patterns that hold for at least three of them. The agent does not call send_campaign, update_campaign or create_campaign.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Use the Klaviyo connection to read campaigns sent in the last 90 days and their metrics. Write campaigns.md ranking them by revenue per recipient, with open rate, click rate and unsubscribe rate, and the subject line, send time and audience patterns shared by the top five. Use only get_campaigns, get_campaign, get_campaign_message, get_campaign_report, get_lists and get_segments. If a statistic is absent from the report, say so instead of filling a zero. Read only: do not send, update, clone or delete a campaign.",
  "connections": [
    "klaviyo"
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is campaigns.md as an artifact on the session. GET /o/{org}/sessions/{id} lists it with a name, a mime type and a url on api.prix.dev that downloads that one file with no key for an hour; reading the session again returns a fresh url. The same file is also at GET /o/{org}/sessions/{id}/artifacts/campaigns.md with the key. The turn's final answer, in result, is a short pointer to the file, not a second copy of the table.

The transcript at GET /o/{org}/sessions/{id}/messages keeps each Klaviyo tool call with its arguments and output, linked by call_id. A reviewer opens the top row, finds the get_campaign_report call for that campaign, and checks that revenue per recipient matches the payload rather than a ratio the agent computed from a different statistic. Subject lines are checked against get_campaign_message, because a campaign and its message are separate objects in these tools.

Before anyone quotes the ranking in a quarterly review, they check three things: the window really is 90 days, unsent campaigns were excluded rather than ranked as zero revenue, and any blank statistic is labeled missing. A follow-up to the same session can ask for a different window. The sandbox parks with its disk when the turn ends, so that follow-up wakes the same run instead of starting over.

Run it on every quarter close

A job on your server fires on the morning after each quarter closes and POSTs this body to /o/{org}/sessions. Put the quarter's end date in the Idempotency-Key header. The same key and body return the run the first call started; the same key with a different body is 409 IDEMPOTENCY_CONFLICT, so a retried close does not write a second report.

The key's creator sets webhook_url with PATCH /o/{org}/api-keys/{id}. When the turn finishes, Rush POSTs run.completed to that URL within about 30 seconds, signed as a Standard Webhook, with session_url and result (the final answer, up to 16 KiB). Your handler verifies the signature, reads the session, and stores the campaigns.md download link. result will not hold the whole report if it is past 16 KiB; the artifact will.

The use case this job follows, with its automation and trigger: Produce a report or file, no repo.

Why run it on Rush?

Pulling a quarter of campaigns by hand means exporting, pasting into a sheet, and redoing the revenue-per-recipient column every time someone asks. This run does the reads in a sandbox that has no checkout and no copy of your Klaviyo key on a laptop. The connection is the only Klaviyo access the run gets, and send_campaign is named in the prompt as forbidden.

The ranking is checkable. Every number in campaigns.md comes from a tool result kept in the transcript, so a marketer can open one campaign in Klaviyo and compare it with the row. Several brands, or several quarters, are several runs. A key allows max_concurrent runs in progress, 4 by default, and the next start is 429, so a backlog waits and retries instead of sharing one session.

Frequently asked questions

Will it send or edit a campaign?

No. The prompt limits the agent to the read-only campaign, list and segment tools and forbids send, update, clone and delete. Klaviyo's own tool table marks get_campaigns, get_campaign, get_campaign_message and get_campaign_report as read-only. A reviewer still scans the transcript for send_campaign before treating the file as final.

What if revenue per recipient is not in the report?

The row says the statistic was not returned, and that campaign is left out of the ranking rather than given a made-up zero. The rest of the table still shows the rates the payload did include. The patterns section only discusses campaigns that had a revenue figure.

Which connection does the name klaviyo match?

A name in connections matches the name GET /me/connections lists for the key's creator. Connect the remote server at https://mcp.klaviyo.com/mcp and use the name you gave it. Two connections with that name are 422 CONNECTION_AMBIGUOUS, which lists their ids; send the id. An unknown or still-pending name is 422 CONNECTION_NOT_READY.

Can I attach a date range other than 90 days?

Yes. The 90 days are in the prompt string, so a caller replaces that window with explicit start and end dates. Keep the rest of the instructions, including the read-only tool list, so a custom range does not become a reason to call send_campaign.