Skip to content
Cowliss is in beta: features and pricing may change.

Quickstart

Cowliss holds the people who use your product and mails them when something happens. Five things:

  1. identify your users.
  2. track what they do.
  3. Send a transactional email from your own code.
  4. Segment them.
  5. Write an automation: a JSON definition, kept in your own repo.

Sign up on the Cowliss dashboard, then invite your team from Settings.

Under Settings → Credentials, create an API key. The secret is shown once, so copy it now. One key serves all your apps.

Every user and event carries the app it came from, so register one first: Apps → New app, or from the CLI:

Terminal window
cow login # browser flow, once
cow apps create --name "my-app" # prints app_my-app
cow apps sources list app_my-app

You end up with two ids. appId (app_my-app) is the product itself: your profiles, segments and automations all belong to it, and it is what cow.json and every list filter name. sourceId (src_…) is what you send on every call. The second command reads it back.

Your key is scoped to the organization, not to one app, so any live src_ in it is accepted: send to another app’s id and the write succeeds with a 200 and the profiles land there. One look costs nothing:

Terminal window
cow whoami --key ak_… --source src_…
{
"data": {
"orgId": "org_2b…",
"app": { "id": "app_my-app", "name": "my-app" },
"sources": [
{ "id": "src_01j2x8…", "kind": "api", "configured": true, "lastReceivedAt": null }
]
}
}

That is the app the pair writes into, and every pipe already feeding it. cow apps sources list <appId> answers the same question from a login instead of a key.

Already have your users in Clerk? Connect it as a second source and they arrive with no integration code, along with a signup and a login event per user.

Terminal window
npm install @cowliss/sdk
import { Cow } from "@cowliss/sdk";
const cow = new Cow({
apiKey: process.env.COW_KEY!,
sourceId: process.env.COW_SOURCE_ID!,
});
await cow.identify({
identifiers: { userId: "user_1" },
traits: { email: "[email protected]", plan: "free" },
});
await cow.track({
identifiers: { userId: "user_1" },
event: "checkout_started",
});

The response carries the appId the profile landed in. Since your sourceId comes from the environment, assert it once at boot: it is the cheap guard against a valid id pointing somewhere you did not mean.

identifiers names who the call is about (userId, anonymousId, email, clerkId). Cowliss owns the profile id and resolves your map to it. There is no alias call: naming two identifiers in one call links them. See Identity. Every field and client option is in the SDK reference.

Writes are synchronous, so the profile is already in Users, with checkout_started on its timeline. Or from the CLI, by any identifier:

Terminal window
cow users find --identifier userId:user_1
cow users events usr_…

A receipt, a password reset, an export that is ready. Your code decides when, so there is no automation and no rule.

await cow.emails.send({
from: "Acme <[email protected]>",
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
});

Already sending with Resend? Point its SDK at Cowliss instead, and the code you have keeps working:

import { Resend } from "resend";
// RESEND_BASE_URL=https://api.cowliss.com/resend/app_app-cowliss-com
// RESEND_API_KEY=<your Cowliss API key>
const resend = new Resend();
await resend.emails.send({
from: "Acme <[email protected]>",
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
});

Either way the message lands in the same delivery log as everything an automation sends. Both can send a template you pushed instead of an inline body. See the Resend SDK section.

A segment is a rule over traits and events, re-evaluated every time somebody is written.

Free-plan users who started a checkout this week:

Terminal window
cow segments create --name "checkout-stalled" --app-id app_my-app --definition '{
"match": "all",
"predicates": [
{ "kind": "trait", "name": "plan", "op": "eq", "value": "free" },
{ "kind": "event", "name": "checkout_started", "op": "performed", "withinDays": 7 }
]
}'

Users → Segments → New segment builds the same thing with a live count. Segments filter your users list, export to your BI tool, and can start an automation the moment somebody enters. See Segments.

An automation is a JSON file in your repo that runs once per person: wait, check, send. (An API call can write the same definition, for a builder or an agent with no repo.)

Terminal window
npm install --save-dev @cowliss/cli
npx cow init && cd .cowliss && npm install

cow init writes the project into .cowliss/ in your repo, and puts app_my-app in its cow.json, since it is your only app. Everything this repo pushes belongs to it, so no file below names it again. It also writes an example, automations/welcome.json with its emails and scenario, sending from your verified domain so it pushes as written. When cow init cannot read one (you are not logged in, or no domain is verified yet), its from says example.com, and a push refuses it until you put an address on your verified domain there or delete the example files.

Two files and one line in cow.json. An email says why it is sent. Your app declares its reasons in .cowliss/cow.json, each with the label a recipient reads:

.cowliss/cow.json
"purposes": [{ "key": "marketing.cartReminders", "label": "Cart reminders" }]

Add it beside the orgId and appId already there. Now the email, which names that purpose. {{firstName|there}} reads the person’s firstName trait:

emails/abandoned-checkout.md
---
subject: Still thinking it over?
purpose: marketing.cartReminders
---
Hi {{firstName|there}}, your cart is still here.

An email can also be Liquid HTML, emails/<key>.html, when it needs your own layout, loops or conditions. See Email templates.

And the automation, triggered by the event you sent in step 3. It gives them a day to come back on their own, and stops if they buy:

automations/abandoned-checkout.json
{
"trigger": { "event": "checkout_started" },
"purpose": "marketing",
"from": "Acme <[email protected]>",
"steps": [
{ "id": "wait", "type": "waitForEvent", "event": "purchase_completed", "timeout": "24h" },
{
"id": "purchased",
"type": "branch",
"if": { "var": "steps.wait.matched" },
"then": [{ "id": "done", "type": "exit" }]
},
{ "id": "remind", "type": "sendEmail", "template": "abandoned-checkout" }
]
}

The wait is durable: it survives deploys, and a purchase at hour 23 still cancels the email.

Terminal window
npx cow plan # check the files and show what a push would change
npx cow push # files a new version of whatever changed
npx cow enable abandoned-checkout

A pushed automation is off until you enable it. Automations then shows its steps, who is in flight and on which step, and every email that went out.

A marketing email reaches only people who said yes to its purpose. Once the push has declared it, record the yes on the identify call you already make: consent: { "marketing.cartReminders": true }. Without it the email is skipped, and the delivery says why. See Consent.

  • Your repo: the full loop, cow init to cow pull.
  • Automations: triggers, the seven steps, rules, and four full examples.
  • Testing: replay an automation on a virtual clock, send nothing.
  • Connect your auth provider: profiles, signups and logins arrive with no integration code.
  • Blueprints: cow add blueprint <name>.
  • API reference: generated from the schemas the server validates with.