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 typeStarts when
eventYou record a custom contact event with the named eventName
contact.createdA contact is created
contact.tag_addedThe named tag is added to a contact
contact.joined_groupA 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 typeEdges in next
send_emailOne { nodeId }
waitOne { nodeId }
wait_for_eventOne matched and one timed_out edge: { nodeId, outcome }
branchTwo or more { nodeId, condition? } edges, tried in order
update_tagsOne { nodeId }
webhookOne { nodeId }
exitNone

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 reentry to true to 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.

create-journey.ts
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 });
Create a journey with REST
curl https://api.samva.dev/v1/journeys \
  -H "x-api-key: $SAMVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @journey.json

Create 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.

edit-journey.ts
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

observe-runs.ts
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

Build and observe journeys
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 --json

create 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

On this page