Journeys (Preview)
Preview · Available to selected organizations. Build automated email sequences that react to contact events, wait, branch, and exit, then publish, pause, and observe them through the SDK, REST API, CLI, or MCP.
A journey is a graph of steps that runs for a contact after a trigger. Each contact moves through the steps one at a time: send an email, wait, wait for an event, branch on the contact, update tags, call a webhook, or exit.
Triggers
A journey starts a run for a contact when one of four triggers fires.
| Trigger type | Starts when |
|---|---|
event | You record a custom contact event with the named eventName |
contact.created | A contact is created |
contact.tag_added | The named tag is added to a contact |
contact.joined_group | A contact joins the group named by groupId |
Define the graph
A definition names its trigger, a rootNodeId, and a nodes record keyed by node ID. A journey
has at most 200 nodes. Every node is reachable from the root, every edge names a node in the graph,
and the graph has no cycles. Branches may converge on a shared node.
Each node has a type, a config, and ordered next edges:
| Node type | Edges in next |
|---|---|
send_email | One { nodeId } |
wait | One { nodeId } |
wait_for_event | One matched and one timed_out edge: { nodeId, outcome } |
branch | Two or more { nodeId, condition? } edges, tried in order |
update_tags | One { nodeId } |
webhook | One { nodeId } |
exit | None |
A branch follows the first edge whose condition holds. Exactly one edge has no condition: it is
the default and must be last, so at least one edge is conditional. A condition is flat:
{ match: "all" | "any", predicates: [...] }, where each predicate tests a tag, a contact
property, a custom field, or whether an event occurred.
wait pauses for durationSeconds and wait_for_event pauses for up to timeoutSeconds. Both are
capped at 90 days.
Send an email
A send_email node names a template and, optionally, a senderId. A node without a senderId
picks its From address the way a send that omits from
does: the template's fromDefault, then your organization's default sender, then your only verified
sender. With several verified senders and no default, that node's send fails and the run ends.
The run sends the template's current publication at the moment the node runs, so publishing a new
template version changes later sends in runs that are already in progress. Template inputs come
from three places: the contact's fields, the trigger event's properties as event.*, and literal
templateData you set on the node. There is no expression language. Contact fields such as name, email, firstName, and customFields
are top-level inputs. Node literals override contact fields; event always contains the trigger
event's properties.
Call a webhook
A webhook node delivers a journey.webhook event to your webhook endpoints
that subscribe to it, then continues. The event's data carries organizationId, journeyId,
journeyVersion, runId, nodeId, the contact (id, externalId, email), and the node's
literal data object, delivered as written. Deliveries are signed and retried like any other
webhook event.
Wait for an event
wait_for_event continues down its matched edge when the contact records the named event after
the run reaches the node, and down timed_out otherwise. Only events recorded after the wait
begins count. The event that started the run and events recorded earlier never satisfy it, and a
back-dated occurredAt does not satisfy it retroactively.
An event predicate in a branch condition or a journey goal looks at the contact's events from
the later of the run's start and 90 days ago.
Run rules
- A contact has at most one active run per journey.
- A contact enters a journey once. Set
reentrytotrueto let the contact enter again after its run exits. - A published definition is immutable, and every run is pinned to the definition it started on.
- A run keeps a snapshot of its trigger event's properties, so
event.*inputs stay available after the event itself passes its retention window. - A matched wait and a goal exit are recorded on the run, so a long pause preserves those outcomes after the matching contact event expires.
Create and publish a journey
This journey sends a welcome email to each new contact, waits two days, and tags contacts who have not clicked.
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const journey = await samva.journeys.create({
name: "Welcome",
definition: {
trigger: { type: "contact.created" },
rootNodeId: "welcome",
nodes: {
welcome: {
type: "send_email",
config: { template: "welcome", senderId: "sender_7q2xk9mvt4znw8rh" },
next: [{ nodeId: "pause" }],
},
pause: { type: "wait", config: { durationSeconds: 172800 }, next: [{ nodeId: "done" }] },
done: { type: "exit", config: {}, next: [] },
},
},
});
await samva.journeys.publish({ id: journey.id, revision: journey.revision });curl https://api.samva.dev/v1/journeys \
-H "x-api-key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d @journey.jsonCreate returns a draft. Nothing runs until you publish. Publishing a draft identical to the newest version changes nothing.
A published journey sends and updates tags as the member who published it. Publishing therefore
needs the permissions those steps need: a key that can send email (messages:create or
email:send) for a send_email node, and contacts:update for an update_tags node. Without
them, publish fails with ForbiddenError (HTTP 403).
Draft, publish, pause
A journey holds a draft you edit and published versions that run. journeys.replaceDraft replaces
the whole draft, and journeys.publish turns it into the next immutable version. Both take the
revision from your last read of the journey. If the journey changed since, the request fails with
ConflictError (HTTP 409): read the journey again and retry.
const current = await samva.journeys.get({ id });
const next = await samva.journeys.replaceDraft({
id,
revision: current.revision,
definition: { ...current.draft, reentry: true },
});
await samva.journeys.publish({ id, revision: next.revision });New runs start on the newest version, and each run stays on the version it started on.
journeys.getVersion returns the definition of any published version.
journeys.pause is an emergency stop. It admits no new contacts and holds every active run before
its next step, including a step that a finished wait would have run. journeys.resume continues
the held runs. A step already executing completes; pause prevents the following step from starting.
journeys.archive ends the journey: it exits every active run with the reason
archived, and an archived journey cannot be edited, published, or resumed. journeys.remove
deletes a journey that was never published or is archived; an active or paused journey returns
ConflictError.
Requests that admit or continue work, such as create, replaceDraft, publish, and resume,
require a plan that includes journeys and return 402 otherwise.
Observe runs
const counts = await samva.journeys.stats({ id });
const page = await samva.journeys.listRuns({ id, status: "active", limit: "25" });
const run = await samva.journeys.getRun({ id, runId: page.items[0]!.id });
for (const step of run.steps) console.log(step.nodeId, step.outcome, step.messageId);stats counts one version's runs by status and, for each node, the runs there now and the runs
that have entered it. A run reports active, paused, exited, or failed. An exited run carries
an exitReason of completed, goal, or archived, and a failed run carries a failureReason.
getRun lists every node the run entered, in order, with how each ended and the message a
send step created. The API reference documents each
operation.
View journeys in the dashboard
The dashboard has a Journeys page that lists each journey with its name, status (Draft, Active, Paused, or Archived), published version, and live run count, which is the active and paused runs on the newest published version. Filter the list by status. Plans without journeys see an upgrade prompt on this page instead of an empty list.
Opening a journey shows its graph on a canvas laid out automatically from the definition: the trigger first, then each step. Branch paths are labelled with their condition or "Otherwise", and event waits are labelled "Event received" and "Timed out". Pan, zoom, fit the graph to the screen, or select a step. The canvas shows the newest published version, or the draft if the journey was never published, and counts of runs per status sit above it.
Selecting a step opens a panel with two views:
- Status shows how many runs are at the step now, have entered it, and have moved on, plus the step's settings and where each path leads.
- Logs lists what recent runs did at the step: the outcome, the time, a link to the email it sent, and a link to the run.
The Runs tab lists the journey's runs newest first, with a status filter. Opening a run shows who it is for, its version, how it ended, the path it took through the graph on the version it started on, and every step it entered with the outcome and times. A contact's page also has a Timeline of their events and journey runs, newest first.
Edit in the dashboard
Build a journey without writing JSON. Choose New journey on the Journeys page, name it, and pick what starts it: a custom event, a created contact, an added tag, or a joined group. The journey opens as a draft with the trigger leading to an exit.
On the canvas, select Edit draft to change the draft. The canvas keeps laying the graph out for you, so you never position a step.
- Add a step. Choose the + on a path to put a step between two others, or Add a step in an empty slot to finish a path. The palette groups the steps by what they do: send email and webhook, wait and wait for event, branch, update tags, and exit. A new step's other paths start empty; every path ends at a step before you can save.
- Join a path to an existing step. An empty slot can lead to a step that is already in the journey, so paths converge. A path cannot lead back to a step it came from, and the dashboard refuses the connection instead of saving a loop.
- Delete a step. Paths into the step carry on to where it led when it has a single way on. A step that ends the journey, or that splits into different places, leaves the paths into it empty, and steps nothing reaches any more are removed with it.
- Set a step up. The Build tab holds the step's settings. A send email step takes a template, a sender (Default sender unless you pick one), and optional literal template data. A wait takes a duration of up to 90 days, and a wait for event takes an event name and how long to wait; its two paths are labelled Event received and Timed out. A branch lists its conditions in order, each matching all or any of its tests on tags, contact properties, custom fields, or events, and ends in a fixed Otherwise path. Add, reorder, and remove the conditions; the default always stays last. An update tags step adds and removes tags, and a webhook step takes the optional data object it delivers. The trigger card holds the trigger, an optional goal event that ends a run early, and whether a contact can enter again.
- Undo and redo. Use the toolbar, or ⌘Z and ⇧⌘Z (Ctrl+Z and Ctrl+Y on Windows and Linux). Delete removes the selected step.
A step with something left to finish is outlined, and the toolbar counts what is left. The dashboard checks the draft against the same rules as the API, including the 200-step limit, so Save draft tells you what to fix instead of failing later.
Save draft replaces the whole draft. It names the revision you last read, so if the journey changed somewhere else, for example through the API, the save fails with a notice instead of overwriting. Choose Reload to see the latest version; reloading discards your unsaved changes. Publish saves any changes and publishes them as the next version. New runs start on it and existing runs stay on the version they entered. Publishing needs the permissions its steps need, so a member who cannot send email cannot publish a journey with a send email step, and the dashboard shows the reason. A plan without journeys can read them but not save or publish.
A journey that is not archived can be paused, resumed, or archived from its page. Pausing holds every live run before its next step, and archiving ends the journey for good after you confirm.
CLI and MCP
samva journeys create --name "Welcome" --definition @welcome.json --json
samva journeys get jrny_7q2xk9mvt4znw8rh --json
samva journeys draft jrny_7q2xk9mvt4znw8rh --revision "$REVISION" --definition @welcome.json
samva journeys publish jrny_7q2xk9mvt4znw8rh --revision "$REVISION"
samva journeys pause jrny_7q2xk9mvt4znw8rh
samva journeys stats jrny_7q2xk9mvt4znw8rh --json
samva journeys runs list jrny_7q2xk9mvt4znw8rh --status active --all --jsonl
samva journeys runs get jrny_7q2xk9mvt4znw8rh jrun_7q2xk9mvt4znw8rh --jsoncreate and draft read the definition from inline JSON, @file, or - for standard input, and
--dry-run parses the request without sending it. archive and delete ask for confirmation, or
--yes in automation. runs list reads one bounded page by default; --all --jsonl streams every
page.
Hosted MCP provides journeys_list, journeys_get, journeys_create, journeys_replace_draft,
journeys_publish, journeys_control (pause, resume, or archive), journeys_stats,
journeys_list_runs, and journeys_get_run. Run results omit the trigger event's properties. The
tools use your credential's organization and API permissions.
Related documentation
Contact events (Preview)
Preview · Available to selected organizations. Record custom contact events, retry them safely, and read each contact's event timeline through the SDK, REST API, CLI, or MCP.
Confirm email subscriptions
Request email subscription confirmation, wait for recipient consent, and add confirmed contacts to a contact group.