Quickstart
Cowliss holds the people who use your product and mails them when something happens. Five things:
identifyyour users.trackwhat they do.- Send a transactional email from your own code.
- Segment them.
- Write an automation: a JSON definition, kept in your own repo.
1. Create an account and an API key
Section titled “1. Create an account and an API key”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.
2. Register an app
Section titled “2. Register an app”Every user and event carries the app it came from, so register one first: Apps → New app, or from the CLI:
cow login # browser flow, oncecow apps create --name "my-app" # prints app_my-appcow apps sources list app_my-appYou 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.
Check the id before you send to it
Section titled “Check the id before you send to it”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:
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.
3. Send identify and track
Section titled “3. Send identify and track”npm install @cowliss/sdkimport { 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" },});
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.
4. See the profile
Section titled “4. See the profile”Writes are synchronous, so the profile is already in Users, with checkout_started on its timeline. Or from the CLI, by any identifier:
cow users find --identifier userId:user_1cow users events usr_…5. Send one transactional email
Section titled “5. Send one transactional email”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({ 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({ 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.
6. Group people with a segment
Section titled “6. Group people with a segment”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:
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.
7. Write an automation and turn it on
Section titled “7. Write an automation and turn it on”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.)
npm install --save-dev @cowliss/clinpx cow init && cd .cowliss && npm installcow 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:
"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:
---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:
{ "trigger": { "event": "checkout_started" }, "purpose": "marketing", "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.
npx cow plan # check the files and show what a push would changenpx cow push # files a new version of whatever changednpx cow enable abandoned-checkoutA 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.
Where next
Section titled “Where next”- Your repo: the full loop,
cow inittocow 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.