Skip to content

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";

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) => { /* ... */ },
});

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.

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.

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.

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.

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.

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.
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("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.

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.

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;

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.

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.

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.

emails/abandoned-checkout.tsx
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.