examples/functions/klaviyo-campaign-create/README.md
Marketing teams need to create email campaigns for their content, but manually creating Klaviyo campaigns and templates for each post is time-consuming and error-prone. This creates delays between content creation and email marketing campaigns.
This Sanity Function automatically creates Klaviyo email campaigns and templates when email content is created or updated in Sanity. When an email document is created, the function generates a Klaviyo template from the content, creates a campaign, and links them together. For updates, it refreshes the template content to keep campaigns synchronized with the latest content changes.
⚠️ Important: This function works in parallel with the Klaviyo Campaign Send 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-create
For an existing project:
npx sanity blueprints add function --example klaviyo-campaign-create
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, KLAVIYO_LIST_ID} = process.env
if (typeof KLAVIYO_API_KEY !== 'string' || typeof KLAVIYO_LIST_ID !== 'string') {
throw new Error('KLAVIYO_API_KEY and KLAVIYO_LIST_ID must be set')
}
export default defineBlueprint({
resources: [
defineDocumentFunction({
name: 'klaviyo-campaign-create',
memory: 1,
timeout: 30,
src: './functions/klaviyo-campaign-create',
event: {
on: ['create', 'update'],
filter: "_type == 'emails' && status != 'sent'",
projection:
'{_id, _type, title, slug, body, marketingCampaign, klaviyoListId, "operation": delta::operation()}',
},
env: {
KLAVIYO_API_KEY: KLAVIYO_API_KEY,
KLAVIYO_LIST_ID: KLAVIYO_LIST_ID,
KLAVIYO_FROM_EMAIL: process.env.KLAVIYO_FROM_EMAIL || '[email protected]',
KLAVIYO_REPLY_TO_EMAIL: process.env.KLAVIYO_REPLY_TO_EMAIL || '[email protected]',
KLAVIYO_CC_EMAIL: process.env.KLAVIYO_CC_EMAIL || '[email protected]',
KLAVIYO_BCC_EMAIL: process.env.KLAVIYO_BCC_EMAIL || '[email protected]',
},
}),
],
})
Install dependencies
Install dependencies in the project root:
npm install dotenv @portabletext/to-html
And install function dependencies:
npm install @sanity/functions
cd functions/klaviyo-campaign-create
npm install
cd ../..
Set up environment variables
Add your Klaviyo credentials to your root .env file:
KLAVIYO_API_KEY: Your Klaviyo private API keyKLAVIYO_LIST_ID: Your Klaviyo list ID for email campaignsKLAVIYO_FROM_EMAIL: Email address for campaign sender (optional)KLAVIYO_REPLY_TO_EMAIL: Reply-to email address (optional)KLAVIYO_CC_EMAIL: CC email address (optional)KLAVIYO_BCC_EMAIL: BCC email address (optional)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-create 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, create a test document first:
# From the studio/ folder, create a test document
cd studio
npx sanity documents create ../functions/klaviyo-campaign-create/document.json --replace
Then test the function with the created document (from project root):
# Back to project root for function testing
cd ..
npx sanity functions test klaviyo-campaign-create --file functions/klaviyo-campaign-create/document.json --dataset production --with-user-token
Alternative: Test with a real document from your dataset:
# From the studio/ folder, find and export an existing document
cd studio
npx sanity documents query "*[_type == 'emails'][0]" > ../real-document.json
# Back to project root for function testing
cd ..
npx sanity functions test klaviyo-campaign-create --file real-document.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 == 'emails'][0]._id" | tr -d '"')
# Create a temporary JSON file with custom data in project root
cd ..
cat > test-custom-data.json << EOF
{
"_type": "emails",
"_id": "$REAL_DOC_ID",
"title": "Custom Test Email Campaign",
"body": [
{
"_type": "block",
"children": [{"_type": "span", "text": "Custom test content"}]
}
],
"status": "inprogress"
}
EOF
# Test with the custom data file
npx sanity functions test klaviyo-campaign-create --file test-custom-data.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 == 'emails' && status != 'sent'][0]" > ../test-real-document.json
# Back to project root for function testing
cd ..
npx sanity functions test klaviyo-campaign-create --file test-real-document.json --dataset production --with-user-token
The function includes comprehensive logging. Check the output for:
// Function logs include:
console.log('👋 Marketing Campaign Function called at', new Date().toISOString())
console.log('✅ Created Klaviyo template:', template.data.id)
console.log('✅ Created Klaviyo campaign:', campaign.data.id)
npx sanity documents query to find suitable test documentsemails document type (from schema-emails.tsx)marketingCampaign document type (from schema-marketing-campaign.tsx)product document type (from e-commerce template)When a content editor creates or updates an email document, the function automatically:
Sample input document:
{
"_type": "emails",
"_id": "test-email-123",
"title": "New Product Launch",
"body": [
{
"_type": "block",
"children": [{"_type": "span", "text": "Check out our new products!"}]
},
{
"_type": "products",
"products": [{"_ref": "product-123", "_type": "reference"}]
}
],
"status": "inprogress",
"klaviyoListId": "optional-list-override"
}
Result: A Klaviyo campaign and template are created with rich HTML content including product information, and a marketing campaign document is created in Sanity to track the relationship.
Update the HTML template generation in the generateEmailTemplate function:
// Customize the email template design
async function generateEmailTemplate(
title: string | undefined,
slug: string | undefined,
body: any[] | undefined,
): Promise<string> {
// Modify the HTML structure, styling, or content
// Add custom components for different content types
// Update branding, colors, and layout
}
Override the default Klaviyo list by setting klaviyoListId on individual email documents:
// In your email document
{
klaviyoListId: 'your-custom-list-id' // Overrides KLAVIYO_LIST_ID env var
}
Extend the portable text rendering to support additional content types:
// In the toHTML components configuration
components: {
types: {
// Add custom content type rendering
customBlock: ({value}) => {
return `<div class="custom-content">${value.content}</div>`
}
}
}
Modify sender details through environment variables:
// Environment variables for email configuration
KLAVIYO_FROM_EMAIL=your-sender@domain.com
KLAVIYO_REPLY_TO_EMAIL=reply@domain.com
Error: "KLAVIYO_API_KEY not found in environment variables"
KLAVIYO_API_KEY=your-api-key to your .env fileError: "Failed to create Klaviyo template: 403 Forbidden"
templates:write scopeError: "Failed to create Klaviyo campaign: 422 Unprocessable Entity"
KLAVIYO_LIST_ID is correct and the list exists in your Klaviyo accountError: "Post title is required for template creation"
title field with contentFunction times out during execution
This function requires two schema files to be added to your Sanity project:
Defines the emails document type with:
Defines the marketingCampaign document type with: