Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion openkiln/skills/smartlead/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ openkiln smartlead delete <campaign_id>
"seq_number": 1,
"seq_delay_details": {"delay_in_days": 0},
"subject": "Hello {{first_name}}",
"email_body": "<div>Your email body here</div>"
"email_body": "<div>Your email body here</div>",
"seq_variants": [
{"subject": "Variant A", "email_body": "<div>A body</div>", "variant_label": "A"},
{"subject": "Variant B", "email_body": "<div>B body</div>", "variant_label": "B"}
]
},
{
"seq_number": 2,
Expand All @@ -106,6 +110,13 @@ openkiln smartlead delete <campaign_id>
]
```

A/B testing: add `seq_variants` to any step. Each variant needs
`subject`, `email_body`, and `variant_label` (A, B, C, etc.).
A top-level `subject` and `email_body` are still required alongside
variants. Steps without `seq_variants` use subject/email_body directly.

The `duplicate` command preserves all variants when copying campaigns.

## Pushing contacts

Push contacts to a Smartlead campaign. Reads contacts from any
Expand Down
27 changes: 22 additions & 5 deletions openkiln/skills/smartlead/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,22 @@ def duplicate(
"seq_number": seq.get("seq_number"),
"seq_delay_details": {"delay_in_days": delay_days},
}
# copy subject/body from top-level or first variant
# copy variants if present, otherwise fall back to top-level subject/body
variants = seq.get("sequence_variants", [])
if variants:
entry["subject"] = variants[0].get("subject", "")
entry["email_body"] = variants[0].get("email_body", "")
active_variants = [v for v in variants if not v.get("is_deleted")]
entry["seq_variants"] = [
{
"subject": v.get("subject", ""),
"email_body": v.get("email_body", ""),
"variant_label": v.get("variant_label", ""),
}
for v in active_variants
]
# API also needs top-level subject/body
first = active_variants[0] if active_variants else {}
entry["subject"] = first.get("subject", "")
entry["email_body"] = first.get("email_body", "")
elif seq.get("subject"):
entry["subject"] = seq.get("subject", "")
entry["email_body"] = seq.get("email_body", "")
Expand Down Expand Up @@ -493,11 +504,17 @@ def sequence(
{
"seq_number": 1,
"seq_delay_details": {"delay_in_days": 0},
"variants": [
{"subject": "...", "email_body": "...", "variant_label": "A"}
"subject": "Default subject",
"email_body": "<div>Default body</div>",
"seq_variants": [
{"subject": "Variant A subject", "email_body": "<div>...</div>", "variant_label": "A"},
{"subject": "Variant B subject", "email_body": "<div>...</div>", "variant_label": "B"}
]
}
]

Steps without seq_variants use subject/email_body directly.
Steps with seq_variants also need a top-level subject/email_body (used as fallback).
"""
content = file.read_text()

Expand Down
Loading