Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Customer.io core features: transactional messages, API-triggered broadcasts, segments, and person merge. Trigger: "customer.io segments", "customer.io transactional", "customer.io broadcast", "customer.io merge users", "customer.io send email".
.claude/skills/jeremylongshore-customerio-core-feature/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-09 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 11% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -8% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 52% | 0% |
Implement Customer.io's key platform features: transactional emails/push (password resets, receipts), API-triggered broadcasts (one-to-many on demand), segment-driving attributes, anonymous-to-known user merging, and person suppression/deletion.
customerio-node installedCUSTOMERIO_SITE_ID + CUSTOMERIO_TRACK_API_KEY)CUSTOMERIO_APP_API_KEY) — required for transactional + broadcastsTransactional messages are opt-in-implied messages (receipts, password resets). Create the template in Customer.io dashboard first, then call the API with data.
typescript// lib/customerio-transactional.ts import { APIClient, SendEmailRequest, RegionUS } from "customerio-node"; const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!, { region: RegionUS, }); // Send a transactional email // transactional_message_id comes from the Customer.io dashboard template async function sendPasswordReset(email: string, userId: string, resetUrl: string) { const request = new SendEmailRequest({ to: email, transactional_message_id: "3", // Template ID from dashboard message_data: { reset_url: resetUrl, expiry_hours: 24, support_email: "help@yourapp.com", }, identifiers: { id: userId }, // Links delivery metrics to user profile }); const response = await api.sendEmail(request); // response = { delivery_id: "abc123", queued_at: 1704067200 } return response; } // Send an order confirmation with complex data async function sendOrderConfirmation( email: string, userId: string, order: { id: string; items: { name: string; qty: number; price: number }[]; total: number } ) { const request = new SendEmailRequest({ to: email, transactional_message_id: "5", message_data: { order_id: order.id, items: order.items, // Accessible as {{ event.items }} in Liquid total: order.total, order_date: new Date().toISOString(), }, identifiers: { id: userId }, }); return api.sendEmail(request); }
Dashboard setup: Create transactional message at Transactional > Create Message. Use Liquid syntax: {{ data.reset_url }}, {{ data.order_id }}, {% for item in data.items %}.
typescriptimport { APIClient, SendPushRequest, RegionUS } from "customerio-node"; const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!, { region: RegionUS, }); async function sendPushNotification(userId: string, title: string, body: string) { const request = new SendPushRequest({ transactional_message_id: "7", // Push template ID identifiers: { id: userId }, message_data: { title, body }, }); return api.sendPush(request); }
Broadcasts send to a pre-defined segment on demand. Define the segment and message in the dashboard, then trigger via API.
typescript// lib/customerio-broadcasts.ts import { APIClient, RegionUS } from "customerio-node"; const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!, { region: RegionUS, }); // Trigger a broadcast to a pre-defined segment async function triggerProductUpdateBroadcast(version: string, changelog: string) { await api.triggerBroadcast( 42, // Broadcast ID from dashboard { version, changelog }, // Data merged into Liquid template { segment: { id: 15 } } // Target segment (defined in dashboard) ); } // Trigger broadcast to specific emails async function triggerBroadcastToEmails( broadcastId: number, emails: string[], data: Record<string, any> ) { await api.triggerBroadcast( broadcastId, data, { emails, email_ignore_missing: true, // Don't error on unknown emails email_add_duplicates: false, // Skip if user already in broadcast } ); } // Trigger broadcast to specific user IDs async function triggerBroadcastToUsers( broadcastId: number, userIds: string[], data: Record<string, any> ) { await api.triggerBroadcast(broadcastId, data, { ids: userIds }); }
Segments in Customer.io are data-driven — they automatically include/exclude people based on attributes you set via identify().
typescript// services/customerio-segments.ts import { TrackClient, RegionUS } from "customerio-node"; const cio = new TrackClient( process.env.CUSTOMERIO_SITE_ID!, process.env.CUSTOMERIO_TRACK_API_KEY!, { region: RegionUS } ); // Update attributes that drive segment membership async function updateEngagementAttributes(userId: string, metrics: { lastActiveAt: Date; sessionCount: number; totalRevenue: number; daysSinceLastLogin: number; }) { await cio.identify(userId, { last_active_at: Math.floor(metrics.lastActiveAt.getTime() / 1000), session_count: metrics.sessionCount, total_revenue: metrics.totalRevenue, days_since_last_login: metrics.daysSinceLastLogin, engagement_tier: metrics.sessionCount > 50 ? "power_user" : metrics.sessionCount > 10 ? "active" : "casual", }); } // Segment examples built in dashboard: // "Power Users": engagement_tier = "power_user" // "At Risk": days_since_last_login > 30 AND plan != "free" // "High Value": total_revenue > 500 // "Trial Expiring": plan = "trial" AND trial_end_at < now + 3 days
typescript// Merge a secondary (duplicate) person into a primary person. // The secondary is deleted permanently after merge. async function mergeUsers( primaryId: string, secondaryId: string, identifierType: "id" | "email" | "cio_id" = "id" ) { await cio.mergeCustomers( identifierType, primaryId, identifierType, secondaryId ); // Secondary person is permanently deleted. // Their attributes and activity merge into the primary. }
typescript// Suppress: user stays in system but receives no messages async function suppressUser(userId: string): Promise<void> { await cio.suppress(userId); } // Delete: remove user entirely from Customer.io async function deleteUser(userId: string): Promise<void> { await cio.destroy(userId); }
| Error | Cause | Solution | |-------|-------|----------| | 422 on transactional | Wrong transactional_message_id | Verify template ID in Customer.io dashboard | | Missing Liquid variable | message_data incomplete | Ensure all {{ data.x }} variables are in message_data | | Broadcast 404 | Invalid broadcast ID | Check broadcast ID in dashboard Broadcasts section | | Segment not updating | Attribute type mismatch | Dashboard segments compare types strictly — number vs string matters | | Merge fails | Secondary person doesn't exist | Verify both people exist before merging |
After implementing core features, proceed to customerio-common-errors for troubleshooting.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-09 | fail→pass | 9,999 | 5,525 | -45% | 1 | 1 | 0% | 1,866 | 3,137 | +68% | 0 | 0 | — |
case-04 | fail→pass | 14,124 | 4,254 | -70% | 1 | 1 | 0% | 2,623 | 2,899 | +11% | 0 | 0 | — |
case-01 | fail→pass | 11,698 | 10,166 | -13% | 1 | 1 | 0% | 2,987 | 4,510 | +51% | 0 | 0 | — |
case-02 | fail→pass | 19,664 | 9,000 | -54% | 1 | 1 | 0% | 4,478 | 4,109 | -8% | 0 | 0 | — |
case-03 | pass→pass | 13,789 | 5,859 | -58% | 1 | 1 | 0% | 2,836 | 3,198 | +13% | 0 | 0 | — |
case-05 | pass→pass | 8,775 | 3,992 | -55% | 1 | 1 | 0% | 1,700 | 2,847 | +67% | 0 | 0 | — |
case-06 | fail→pass | 8,595 | 4,216 | -51% | 1 | 1 | 0% | 1,890 | 2,876 | +52% | 0 | 0 | — |
case-07 | pass→pass | 7,757 | 4,703 | -39% | 1 | 1 | 0% | 1,410 | 2,890 | +105% | 0 | 0 | — |
case-08 | pass→pass | 16,075 | 4,895 | -70% | 1 | 1 | 0% | 1,672 | 3,086 | +85% | 0 | 0 | — |
case-10 | fail→pass | 9,804 | 4,280 | -56% | 1 | 1 | 0% | 1,685 | 2,849 | +69% | 0 | 0 | — |
case-11 | fail→pass | 7,982 | 3,517 | -56% | 1 | 1 | 0% | 1,401 | 2,592 | +85% | 0 | 0 | — |
case-12 | pass→pass | 7,144 | 3,630 | -49% | 1 | 1 | 0% | 1,283 | 2,693 | +110% | 0 | 0 | — |
case-13 | pass→pass | 10,101 | 3,993 | -60% | 1 | 1 | 0% | 1,757 | 2,660 | +51% | 0 | 0 | — |
case-14 | pass→pass | 3,366 | 2,975 | -12% | 1 | 1 | 0% | 633 | 2,660 | +320% | 0 | 0 | — |
case-15 | pass→pass | 11,233 | 6,106 | -46% | 1 | 1 | 0% | 1,880 | 3,089 | +64% | 0 | 0 | — |
case-16 | fail→pass | 12,273 | 5,338 | -57% | 1 | 1 | 0% | 2,357 | 3,181 | +35% | 0 | 0 | — |
case-17 | pass→pass | 4,123 | 3,602 | -13% | 1 | 1 | 0% | 756 | 2,741 | +263% | 0 | 0 | — |
case-18 | pass→pass | 8,673 | 3,629 | -58% | 1 | 1 | 0% | 1,644 | 2,611 | +59% | 0 | 0 | — |
case-19 | pass→pass | 8,679 | 2,497 | -71% | 1 | 1 | 0% | 1,680 | 2,517 | +50% | 0 | 0 | — |
case-20 | pass→pass | 17,085 | 11,615 | -32% | 1 | 1 | 0% | 3,354 | 4,525 | +35% | 0 | 0 | — |
case-21 | pass→pass | 15,909 | 13,693 | -14% | 1 | 1 | 0% | 3,046 | 4,773 | +57% | 0 | 0 | — |
case-22 | pass→pass | 11,232 | 5,626 | -50% | 1 | 1 | 0% | 2,067 | 2,940 | +42% | 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 +36 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.