Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Customer.io primary messaging workflow. Use when setting up campaign triggers, welcome sequences, onboarding flows, or event-driven email automation. Trigger: "customer.io campaign", "customer.io workflow", "customer.io email automation", "customer.io messaging", "customer.io onboarding".
.claude/skills/jeremylongshore-customerio-primary-workflow/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 31 |
| gemini-3.1-pro-preview | 100% | 1 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 37% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 15% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 100% | 0% |
Implement Customer.io's core messaging workflow: identify users with segment-ready attributes, track lifecycle events that trigger campaigns, and set up the data layer for automated onboarding, nurture, and re-engagement sequences.
customerio-node configured with Track API credentialsYour App (SDK) Customer.io Dashboard User
───────────── ──────────────────── ────
cio.identify(user) → Profile created/updated
cio.track("signed_up") → Campaign trigger fires
Wait 1 day → Welcome email
Check: verified?
├─ No → Verification reminder
└─ Yes → Wait 3 days → Feature tips emailEvents tracked via the SDK trigger campaigns you build in the dashboard. The SDK sends the data; the dashboard defines the workflow logic.
typescript// lib/customerio-events.ts import { TrackClient, RegionUS } from "customerio-node"; // Central event definitions — every event your app tracks export const CIO_EVENTS = { // Onboarding SIGNED_UP: "signed_up", EMAIL_VERIFIED: "email_verified", PROFILE_COMPLETED: "profile_completed", FIRST_PROJECT_CREATED: "first_project_created", // Engagement FEATURE_USED: "feature_used", INVITED_TEAMMATE: "invited_teammate", UPGRADE_STARTED: "upgrade_started", UPGRADE_COMPLETED: "upgrade_completed", // Lifecycle SUBSCRIPTION_RENEWED: "subscription_renewed", SUBSCRIPTION_CANCELLED: "subscription_cancelled", TRIAL_EXPIRING: "trial_expiring", // Commerce CHECKOUT_STARTED: "checkout_started", CHECKOUT_COMPLETED: "checkout_completed", REFUND_REQUESTED: "refund_requested", } as const; type EventName = (typeof CIO_EVENTS)[keyof typeof CIO_EVENTS];
typescript// services/customerio-messaging.ts import { TrackClient, RegionUS } from "customerio-node"; import { CIO_EVENTS } from "../lib/customerio-events"; const cio = new TrackClient( process.env.CUSTOMERIO_SITE_ID!, process.env.CUSTOMERIO_TRACK_API_KEY!, { region: RegionUS } ); interface UserProfile { id: string; email: string; firstName: string; lastName?: string; plan: string; companyName?: string; } export class MessagingService { /** Call on user signup — creates profile and triggers onboarding campaign */ async onSignup(user: UserProfile, signupMethod: string): Promise<void> { // 1. Identify with all attributes campaigns need await cio.identify(user.id, { email: user.email, first_name: user.firstName, last_name: user.lastName ?? "", plan: user.plan, company: user.companyName ?? "", created_at: Math.floor(Date.now() / 1000), onboarding_step: "signed_up", }); // 2. Track the event that triggers the onboarding campaign await cio.track(user.id, { name: CIO_EVENTS.SIGNED_UP, data: { method: signupMethod, // "google", "email", "github" plan: user.plan, }, }); } /** Call when user verifies email — updates profile + tracks event */ async onEmailVerified(userId: string): Promise<void> { await cio.identify(userId, { email_verified: true, email_verified_at: Math.floor(Date.now() / 1000), onboarding_step: "verified", }); await cio.track(userId, { name: CIO_EVENTS.EMAIL_VERIFIED, }); } /** Call on feature usage — drives engagement segments and campaigns */ async onFeatureUsed( userId: string, feature: string, metadata?: Record<string, any> ): Promise<void> { await cio.track(userId, { name: CIO_EVENTS.FEATURE_USED, data: { feature, ...metadata }, }); // Update engagement metrics on the profile for segmentation await cio.identify(userId, { last_active_at: Math.floor(Date.now() / 1000), }); } /** Call on plan upgrade — triggers upgrade confirmation campaign */ async onUpgrade(userId: string, from: string, to: string, mrr: number): Promise<void> { await cio.identify(userId, { plan: to, mrr, upgraded_at: Math.floor(Date.now() / 1000), }); await cio.track(userId, { name: CIO_EVENTS.UPGRADE_COMPLETED, data: { from_plan: from, to_plan: to, mrr }, }); } /** Call on cancellation — triggers win-back campaign */ async onCancellation(userId: string, reason: string): Promise<void> { await cio.identify(userId, { plan: "cancelled", cancelled_at: Math.floor(Date.now() / 1000), cancellation_reason: reason, }); await cio.track(userId, { name: CIO_EVENTS.SUBSCRIPTION_CANCELLED, data: { reason }, }); } }
typescript// routes/auth.ts (Express example) import { MessagingService } from "../services/customerio-messaging"; const messaging = new MessagingService(); router.post("/signup", async (req, res) => { const user = await db.createUser(req.body); // Fire-and-forget — don't block the signup response messaging.onSignup( { id: user.id, email: user.email, firstName: user.firstName, plan: user.plan, }, req.body.signupMethod ).catch((err) => console.error("CIO signup tracking failed:", err)); res.json({ user }); }); router.post("/verify-email", async (req, res) => { await db.verifyEmail(req.user.id); messaging.onEmailVerified(req.user.id).catch(console.error); res.json({ verified: true }); });
In Customer.io dashboard, create campaigns triggered by these events:
Onboarding Campaign:
signed_up{{ customer.first_name }} and {{ event.method }} Liquid)email_verified true?first_project_created event?Cancellation Win-back Campaign:
subscription_cancelled{{ event.reason }} Liquid variable| Variable | Source | Example | |----------|--------|---------| | {{ customer.first_name }} | identify() attributes | "Jane" | | {{ customer.plan }} | identify() attributes | "pro" | | {{ event.method }} | track() event data | "google" | | {{ event.reason }} | track() event data | "too_expensive" |
| Error | Cause | Solution | |-------|-------|----------| | Campaign not triggering | Event name mismatch | Event names are case-sensitive — verify exact match | | User not receiving email | Missing email attribute | Always include email in identify() | | Duplicate sends | Multiple event fires | Use fire-and-forget with deduplication | | Liquid rendering {{ }} | Missing data property | Ensure data object has all template variables |
After implementing primary workflow, proceed to customerio-core-feature for transactional messages, segments, and broadcasts.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,487 | 10,992 | -24% | 1 | 1 | 0% | 3,502 | 4,814 | +37% | 0 | 0 | — |
case-02 | fail→pass | 15,021 | 10,617 | -29% | 1 | 1 | 0% | 3,147 | 4,550 | +45% | 0 | 0 | — |
case-03 | fail→pass | 22,754 | 15,769 | -31% | 1 | 1 | 0% | 5,025 | 5,801 | +15% | 0 | 0 | — |
case-08 | fail→pass | 16,433 | 16,306 | -1% | 1 | 1 | 0% | 2,819 | 5,225 | +85% | 0 | 0 | — |
case-04 | pass→pass | 6,905 | 5,008 | -27% | 1 | 1 | 0% | 1,321 | 3,151 | +139% | 0 | 0 | — |
case-05 | pass→pass | 11,717 | 8,610 | -27% | 1 | 1 | 0% | 2,439 | 4,003 | +64% | 0 | 0 | — |
case-06 | pass→pass | 9,001 | 6,185 | -31% | 1 | 1 | 0% | 1,868 | 3,458 | +85% | 0 | 0 | — |
case-07 | fail→fail | 17,856 | 20,315 | +14% | 1 | 1 | 0% | 3,430 | 6,169 | +80% | 0 | 0 | — |
case-09 | pass→pass | 14,105 | 10,785 | -24% | 1 | 1 | 0% | 2,559 | 4,179 | +63% | 0 | 0 | — |
case-10 | pass→pass | 5,511 | 3,731 | -32% | 1 | 1 | 0% | 1,090 | 2,992 | +174% | 0 | 0 | — |
case-11 | fail→pass | 11,041 | 9,283 | -16% | 1 | 1 | 0% | 2,032 | 4,071 | +100% | 0 | 0 | — |
case-12 | fail→pass | 19,760 | 13,040 | -34% | 1 | 1 | 0% | 4,235 | 5,333 | +26% | 0 | 0 | — |
case-13 | fail→fail | 11,924 | 8,762 | -27% | 1 | 1 | 0% | 2,324 | 3,898 | +68% | 0 | 0 | — |
case-14 | pass→pass | 11,942 | 7,590 | -36% | 1 | 1 | 0% | 2,159 | 3,724 | +72% | 0 | 0 | — |
case-15 | fail→pass | 14,264 | 10,700 | -25% | 1 | 1 | 0% | 2,744 | 4,256 | +55% | 0 | 0 | — |
case-16 | fail→pass | 10,855 | 7,924 | -27% | 1 | 1 | 0% | 2,037 | 3,743 | +84% | 0 | 0 | — |
case-17 | fail→fail | 17,064 | 13,369 | -22% | 1 | 1 | 0% | 2,913 | 4,603 | +58% | 0 | 0 | — |
case-18 | pass→pass | 10,131 | 8,428 | -17% | 1 | 1 | 0% | 1,932 | 3,741 | +94% | 0 | 0 | — |
case-19 | fail→pass | 12,503 | 9,038 | -28% | 1 | 1 | 0% | 2,176 | 4,017 | +85% | 0 | 0 | — |
case-20 | pass→pass | 10,179 | 11,844 | +16% | 1 | 1 | 0% | 2,437 | 4,684 | +92% | 0 | 0 | — |
case-21 | pass→pass | 12,670 | 10,283 | -19% | 1 | 1 | 0% | 2,522 | 4,426 | +75% | 0 | 0 | — |
case-22 | pass→pass | 16,221 | 13,852 | -15% | 1 | 1 | 0% | 3,593 | 5,175 | +44% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.