Journey API reference
The complete authoring surface, for when you have a journey file open. If you are meeting journeys for the first time, read Journeys first: this page is the lookup, that one is the story.
Two packages, types only at the edges, no runtime dependency on Cowliss inside your bundle:
import { CapabilityError, defineJourney } from "@cowliss/cli/journeys";import type { Api, Duration, Event, Profile } from "@cowliss/cli/journeys";import type { SendClass, Subject } from "@cowliss/cli/emails";defineJourney(config)
Section titled “defineJourney(config)”Returns the journey. Validates the config with the same schema the release manifest is validated with, so an unknown purpose or a malformed trigger throws while you are building, not at the first execution. Export the result as the module’s default; the journey’s key is the file name.
| Field | Type | Required | Meaning |
|---|---|---|---|
trigger |
{ event, appId? } or { segment } |
yes | What starts an execution. event, appId and segment are patterns: one string, or a list of them read as a disjunction. Omitting appId spans every app. A segment trigger fires on entry. |
purpose |
"emailMarketing" | "dataProcessing" |
yes | The consent purpose every send from this journey is gated on. |
environments |
Environment[] |
no | Your own rollout gate. Both environments when omitted; list one to ship to development first. |
tags |
string[] |
no | Your labels, up to 20, each at most 50 characters, kept exactly as written. Duplicates collapse. None when omitted. |
run |
(event, api) => Promise<void> |
yes | The journey body. event is the trigger event, api is the capability object below. |
export default defineJourney({ trigger: { event: "checkout_started" }, purpose: "emailMarketing", run: async (event, api) => { /* ... */ },});The api object
Section titled “The api object”Eleven calls. Every one except log is asynchronous and journaled: the platform records the call and its result, and a replay returns the recorded result rather than doing the work again. That is what makes a journey resumable, and it is why the same call with the same arguments must be reached in the same order on every replay. See Determinism.
api.sleep(duration)
Section titled “api.sleep(duration)”await api.sleep("2d");duration |
Duration: an ms-style string ("30s", "1h", "2d", "1w") or a whole number of milliseconds. |
| Returns | Promise<void>, once the deadline passes. |
A durable wait. The execution is not running while it sleeps, and a sleep of weeks costs nothing to hold open. Events that arrive during it are still delivered to a later waitForEvent.
api.waitForEvent(pattern, { timeout })
Section titled “api.waitForEvent(pattern, { timeout })”const paid = await api.waitForEvent(["order_paid", "invoice_paid"], { timeout: "48h",});pattern |
string | string[]. * matches any run of characters, dots included. A list is a disjunction. |
options.timeout |
Duration. Required: a wait always has a deadline. |
| Returns | Promise<Event | null>: the first matching event, or null when the timeout elapses first. |
The event carries its own name, so a wait on a list can branch on which one arrived. A pattern whose literal prefix is not system. never resolves with an event Cowliss wrote itself, so "*" means every event your apps sent and nothing else. See Event patterns.
api.email.send({ template, props })
Section titled “api.email.send({ template, props })”const { status } = await api.email.send({ template: "abandoned-checkout", props: { firstName: me.traits.firstName as string | undefined },});template |
The template’s key, which is its file name under emails/. Typed against your own project once cow build has written .cow/types.d.ts. |
props |
The props that template declares, checked against its zod schema before anything renders. |
| Returns | Promise<EmailSendResult>: { deliveryId, status }. |
| Throws | unknown_template when the release has no template by that key, or the template’s own render error when the props do not satisfy its schema. |
A send call is an attempt, not a send. It is evaluated against the send gates at the moment it runs, so an unsubscribe halfway through a journey is respected immediately. A gate that stops it resolves normally with a skipped_* status rather than throwing: read status if the next step depends on the mail actually going out. See Sending and delivery.
api.webhook.send({ destination, payload })
Section titled “api.webhook.send({ destination, payload })”await api.webhook.send({ destination: "crm", payload: { profileId: me.id } });destination |
The name of a destination registered in this environment, not a URL. |
payload |
Any JSON object. Delivered as a signed POST. |
| Returns | Promise<void>. |
| Throws | capability_failed when the destination is unknown or the endpoint rejects the delivery after its retries. |
api.traits.set(key, value) and api.traits.unset(key)
Section titled “api.traits.set(key, value) and api.traits.unset(key)”await api.traits.set("cart_recovered", true);await api.traits.unset("at_risk");set(key, value) |
Writes one trait on the recipient. A null value deletes the key, the same merge rule identify follows; false stores false. |
unset(key) |
Removes the key entirely, which is distinct from false for an exists predicate. |
| Returns | Promise<void>. |
A trait write recomputes the profile’s segments, so it can move the profile in or out of a segment and start another journey. That is the intended way to compose journeys, and the reason for the loop rule in Journey composition.
api.profile.get()
Section titled “api.profile.get()”const me = await api.profile.get();if (me.segments.includes("vip")) { /* ... */ }| Returns | Promise<Profile>: the recipient of this execution. |
A fresh read every time it is called, never the state at the trigger. Call it again after a long sleep rather than holding the old value.
api.profiles.get(id)
Section titled “api.profiles.get(id)”const inviter = await api.profiles.get(me.traits.invitedBy as string);id |
A usr_ profile id in the same organization and environment. |
| Returns | Promise<Profile>. |
| Throws | not_found for an id outside this environment, which is also what an id from another organization returns. |
api.events.track(name, properties)
Section titled “api.events.track(name, properties)”await api.events.track("recovery_completed", { orderId: "o_1" });name |
Your own event name. It may not start with system., which is reserved for the events Cowliss writes itself. |
properties |
Any JSON object. |
| Returns | Promise<void>. |
| Throws | reserved_event_name for a system. prefix, before anything is written. |
The event lands on the recipient’s timeline and can trigger other journeys, including this one. Mind the loop rule.
api.log(...args)
Section titled “api.log(...args)”api.log("waited", paid ? "and it arrived" : "in vain");The one call that is not journaled and not counted against the capability limit. Lines are captured per execution and shown on the execution page: 100 lines of up to 1 KB each, after which further lines are dropped. Replayed steps do not re-report their logs, so what you see is each line once.
api.restart({ event? })
Section titled “api.restart({ event? })”const active = await api.waitForEvent("*", { timeout: "30d" });if (active) { await api.restart({ event: active });}args.event |
Optional. The event the fresh execution starts with. |
| Returns | Promise<never>. It does not resolve: nothing after it runs. |
Ends the execution and starts a new one under the same workflow id with an empty journal. This is how a journey loops for longer than one journal can hold. See Long loops and the activity-decay example.
Errors
Section titled “Errors”import { CapabilityError } from "@cowliss/cli/journeys";
try { await api.webhook.send({ destination: "crm", payload });} catch (error) { if (error instanceof CapabilityError) { api.log("crm rejected it:", error.code); }}CapabilityError has a code and a message. The failure is journaled like any other result, so a journey that catches one takes the same branch on every replay. An uncaught error fails the execution, which is then visible on its page with the journal that led there.
code |
When |
|---|---|
unknown_template |
api.email.send named a template this release does not contain. |
not_found |
api.profiles.get named a profile outside this environment. |
reserved_event_name |
api.events.track named an event starting with system.. |
capability_unavailable |
Journey code reached for something a journey does not have, such as fetch or setTimeout. |
capability_failed |
The platform attempted the call and it failed. The message carries the cause. |
A template that throws while rendering surfaces its own code and message the same way.
type Event = { name: string; properties: Record<string, unknown>; /** Milliseconds since the epoch. */ timestamp: number;};
type Profile = { /** The Cowliss-generated profile id (usr_). */ id: string; traits: Record<string, unknown>; consent: Record<ConsentPurpose, boolean>; identifiers: Record<string, string>; /** Names of the segments the profile is currently in. */ segments: string[];};
type EmailSendResult = { deliveryId: string; status: string };
/** An ms-style string ("2d") or a whole number of milliseconds. */type Duration = string | number;What a journey does not have
Section titled “What a journey does not have”Journey code runs sealed: no network, no filesystem, no environment variables, no clock of its own. Reaching for one is a capability_unavailable error rather than a silent difference between the first run and the replay.
Date.now(), new Date() |
The journey’s own clock. Frozen inside a step, advanced at each journaled call, identical on every replay. |
Math.random() |
Seeded from the execution id, so a replay draws the same numbers. |
setTimeout, setInterval, setImmediate, queueMicrotask |
Unavailable. Use api.sleep. |
fetch, XMLHttpRequest |
Unavailable. Use api.webhook.send or a destination. |
Everything a journey reaches the world with goes through api, which is exactly the set of calls above.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Capability calls per execution | 1,000 |
| Journal size per execution | 1 MB |
| Captured log lines per execution | 100, of 1 KB each |
Per-step time, memory, and payload limits are in Journeys. When a long-running journey approaches the call or journal cap, api.restart resets both.
The email template contract
Section titled “The email template contract”A template is one file under emails/, and its key is the file name. cow build reads the metadata below, turns props into JSON Schema for the manifest and into the types that check api.email.send, and the platform renders default from the pinned release.
import type { SendClass, Subject } from "@cowliss/cli/emails";import { Html, Text } from "@react-email/components";import { z } from "zod";
export const props = z.object({ firstName: z.string().optional() });
export const subject: Subject<typeof props> = (p) => p.firstName ? `${p.firstName}, your cart is waiting` : "Your cart is waiting";
export const sendClass: SendClass = "marketing";
export const tags = ["commerce"];
export default function AbandonedCheckout(p: z.infer<typeof props>) { return <Html><Text>Hi {p.firstName ?? "there"}.</Text></Html>;}| Export | Type | Required | Meaning |
|---|---|---|---|
default |
(props) => ReactElement |
yes | The React Email component. |
props |
z.ZodType |
yes | The props schema. It types the send call and validates at send time. |
subject |
string or (props) => string |
yes | The subject line, computed from the same props. |
sendClass |
"marketing" | "transactional" |
no | "marketing" when omitted. A transactional template skips the consent gate and the frequency cap. |
tags |
string[] |
no | Your labels, on the same terms as a journey’s. |
verifyLink |
boolean |
no | Asks the platform to mint a signed verifyUrl prop at send time. |
The full authoring guide, including sharing layout between templates and what renders where, is in Email templates.