examples/functions/klaviyo-campaign-send/README.md
Marketing teams create email campaigns but need a way to automatically send them when they're ready. Manually triggering campaign sends in Klaviyo creates delays and potential human error in the email marketing workflow.
This Sanity Function automatically sends Klaviyo email campaigns when a marketing campaign document status is changed to 'ready' in Sanity. The function triggers the campaign send job in Klaviyo and updates both the marketing campaign and email document statuses to 'sent' to prevent duplicate sends.
⚠️ Important: This function works in parallel with the Klaviyo Campaign Create Function. Both functions are required for a complete email marketing workflow:
You must install and configure both functions for successful campaign management.
This function is built to be compatible with the Sanity E-commerce template. It works specifically with product data and is designed for e-commerce email marketing campaigns.
Important: Run these commands from the root of your project (not inside the studio/ folder).
Initialize the example
For a new project:
npx sanity blueprints init --example klaviyo-campaign-send
For an existing project:
npx sanity blueprints add function --example klaviyo-campaign-send
You'll be prompted to select your organization and Sanity studio.
Add schema types to your project
Copy the schema files to your project:
schema-emails.tsx - Defines the email document type with rich content supportschema-marketing-campaign.tsx - Defines the marketing campaign document typeAdd them to your schema in sanity.config.ts:
import {emailsType} from './schema-emails'
import {marketingCampaignType} from './schema-marketing-campaign'
export default defineConfig({
// ... other config
schema: {
types: [
// ... your existing types
emailsType,
marketingCampaignType,
],
},
})
Add configuration to your blueprint
// sanity.blueprint.ts
import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'
import 'dotenv/config'
import process from 'node:process'
const {KLAVIYO_API_KEY} = process.env
if (typeof KLAVIYO_API_KEY !== 'string') {
throw new Error('KLAVIYO_API_KEY must be set')
}
export default defineBlueprint({
resources: [
defineDocumentFunction({
name: 'klaviyo-campaign-send',
memory: 1,
timeout: 30,
src: './functions/klaviyo-campaign-send',
event: {
on: ['update'],
filter: "_type == 'marketingCampaign' && status == 'ready'",
projection: '{_id, _type, title, email, klaviyoCampaignId}',
},
env: {
KLAVIYO_API_KEY: KLAVIYO_API_KEY,
},
}),
],
})
Install dependencies
Install dependencies in the project root:
npm install dotenv
And install function dependencies:
npm install @sanity/functions
cd functions/klaviyo-campaign-send
npm install
cd ../..
Set up environment variables
Add your Klaviyo credentials to your root .env file:
KLAVIYO_API_KEY: Your Klaviyo private API key (requires campaigns:write scope)Deploy your schema
From the studio folder, deploy your updated schema:
# From the studio/ folder (adjust path as needed for template structure)
cd studio
npx sanity schema deploy
cd ..
You can test the klaviyo-campaign-send function locally using the Sanity CLI before deploying it to production.
Important: Document functions require that the document ID used in testing actually exists in your dataset. The examples below show how to work with real document IDs.
Since document functions require the document ID to exist in your dataset, you'll need an existing marketing campaign document:
# From the studio/ folder, find an existing marketing campaign
cd studio
npx sanity documents query "*[_type == 'marketingCampaign'][0]" > ../real-campaign.json
# Back to project root for function testing
cd ..
npx sanity functions test klaviyo-campaign-send --file real-campaign.json --dataset production --with-user-token
Alternative: Create a test marketing campaign document first:
# From the studio/ folder, create a test marketing campaign
cd studio
cat > test-marketing-campaign.json << EOF
{
"_type": "marketingCampaign",
"title": "Test Campaign Send",
"status": "ready",
"klaviyoCampaignId": "your-klaviyo-campaign-id",
"email": {"_ref": "existing-email-id", "_type": "reference"},
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
"updatedAt": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
}
EOF
npx sanity documents create test-marketing-campaign.json --replace
# Back to project root for function testing
cd ..
npx sanity functions test klaviyo-campaign-send --file studio/test-marketing-campaign.json --dataset production --with-user-token
Start the development server for interactive testing:
npx sanity functions dev
This opens an interactive playground where you can test functions with custom data.
For custom data testing, you still need to use a real document ID that exists in your dataset:
# From the studio/ folder, create or find a real document ID
cd studio
REAL_DOC_ID=$(npx sanity documents query "*[_type == 'marketingCampaign'][0]._id" | tr -d '"')
# Create a temporary JSON file with custom data in project root
cd ..
cat > test-custom-campaign.json << EOF
{
"_type": "marketingCampaign",
"_id": "$REAL_DOC_ID",
"title": "Custom Test Campaign",
"status": "ready",
"klaviyoCampaignId": "your-test-campaign-id",
"email": {"_ref": "existing-email-id", "_type": "reference"}
}
EOF
# Test with the custom data file
npx sanity functions test klaviyo-campaign-send --file test-custom-campaign.json --dataset production --with-user-token
The most reliable approach is to test with existing documents from your dataset:
# From the studio/ folder, find and export a document that matches your function's filter
cd studio
npx sanity documents query "*[_type == 'marketingCampaign' && status == 'ready'][0]" > ../test-real-campaign.json
# Back to project root for function testing
cd ..
npx sanity functions test klaviyo-campaign-send --file test-real-campaign.json --dataset production --with-user-token
The function includes comprehensive logging. Check the output for:
// Function logs include:
console.log('🚀 Marketing Campaign Send Function called at', new Date().toISOString())
console.log('📢 Sending Klaviyo campaign:', klaviyoCampaignId)
console.log('✅ Campaign send job created successfully:', sendJobResponse.data.id)
npx sanity documents query to find suitable test documentsmarketingCampaign document type (from schema-marketing-campaign.tsx)emails document type (from schema-emails.tsx)When a marketing campaign document status is changed to 'ready', the function automatically:
Sample input document:
{
"_type": "marketingCampaign",
"_id": "marketing-campaign-123",
"title": "Product Launch Campaign",
"status": "ready",
"klaviyoCampaignId": "abc123",
"klaviyoTemplateId": "def456",
"email": {
"_ref": "email-123",
"_type": "reference"
},
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-01-15T11:00:00.000Z"
}
Result: The Klaviyo campaign is sent, and both the marketing campaign and associated email document are marked as 'sent' with timestamps.
The function includes comprehensive error handling for common Klaviyo API errors:
// Rate limiting
if (sendCampaignResponse.status === 429) {
console.error('❌ Rate limit exceeded. Klaviyo allows 10/s burst, 150/m steady')
}
// Permission errors
if (sendCampaignResponse.status === 403) {
console.error('❌ Forbidden. Check API key permissions (campaigns:write scope required)')
}
// Campaign not ready
if (sendCampaignResponse.status === 422) {
console.error('❌ Unprocessable entity. Campaign may not be ready to send')
}
Modify the status update logic to include additional fields:
// Update marketing campaign with custom fields
await client
.patch(_id, {
set: {
status: 'sent',
sentAt: new Date().toISOString(),
sentBy: 'automated-function', // Custom field
updatedAt: new Date().toISOString(),
},
})
.commit()
Add custom validation before sending campaigns:
// Add validation logic before sending
if (!emailDocument.title || emailDocument.title.trim().length === 0) {
console.error('❌ Email must have a title before sending')
return
}
// Check for required content
if (!emailDocument.body || emailDocument.body.length === 0) {
console.error('❌ Email must have content before sending')
return
}
Error: "KLAVIYO_API_KEY not found in environment variables"
KLAVIYO_API_KEY=your-api-key to your .env fileError: "Failed to send Klaviyo campaign: 403 Forbidden"
campaigns:write scopeError: "Failed to send Klaviyo campaign: 422 Unprocessable Entity"
Error: "Failed to send Klaviyo campaign: 429 Rate limit exceeded"
Error: "Email document not found"
Error: "Klaviyo campaign ID not found in marketing campaign document"
This function is part of a complete email marketing workflow:
Use the emails document type to create rich email content with products, images, and text.
The klaviyo-campaign-create function automatically creates Klaviyo campaigns and templates from email content.
Review the created marketing campaign document and update its status to 'ready' when ready to send.
This function automatically sends the campaign when the status changes to 'ready'.
emails: inprogress → ready-for-review → ready → sent
marketingCampaign: draft → ready → sent