Waymark / Docs GitHub

Overview

Waymark is a schema-driven journey orchestration engine. Instead of encoding multi-step workflow logic inside your application code, you define journeys as structured JSON documents — and Waymark's runtime engine evaluates them on every step submission.

This means product and operations teams can modify journey branching logic, add compliance checks, and create new flows without engineering deployments.

Waymark is engine-agnostic. Any HTTP client can consume the REST API — React, Vue, React Native, Node.js, or a plain shell script.

Key concepts at a glance

ConceptDescription
FlowA versioned container of nodes and connections. The journey definition.
NodeA single step. Its type controls rendering; jsonContent provides configuration.
ConnectionA directed edge between nodes, with optional condition evaluated against submitted data.
SessionA runtime instance of a flow for a specific customer.
SubmissionThe recorded payload for each completed node.
CustomerProfileOptional profile that conditions can reference (country, risk tier, metadata).

Getting Started

Prerequisites

  • .NET SDK 10.x
  • Node.js 22.x (for frontend)
  • Docker (for local PostgreSQL)

1. Clone the repository

bash
git clone https://github.com/MaximumTrainer/Waymark
cd Waymark

2. Start local dependencies

bash
docker compose up -d

Local PostgreSQL defaults: host localhost:5432, database onboarding, user/password postgres/postgres.

3. Start the backend API

bash
ConnectionStrings__OnboardingDb="Host=localhost;Port=5432;Database=onboarding;Username=postgres;Password=postgres" \
  dotnet run --project src/backend/OpenOnboarding.Api

Swagger UI is available at http://localhost:5072/swagger (or https://localhost:7252/swagger) in Development mode.

4. Start the frontend

bash
cd src/frontend
cp .env.example .env.local
# Set VITE_API_BASE_URL=http://localhost:5072
# Set VITE_API_KEY=dev-api-key-change-in-production
npm install
npm run dev

5. Create your first flow

http
POST /api/flows
Content-Type: application/json
X-Api-Key: dev-api-key-change-in-production

{
  "name": "My First Journey",
  "nodes": [
    {
      "id": "11111111-1111-1111-1111-111111111111",
      "key": "welcome",
      "type": "Information",
      "title": "Welcome! This is step one.",
      "isStartNode": true
    }
  ],
  "connections": []
}

Architecture

Waymark follows a Ports & Adapters (Hexagonal) architecture. Business rules live in the domain and application layers. Infrastructure adapters (database, HTTP, webhooks) depend on inward-facing ports — never the other way around.

LayerProjectResponsibility
DomainOpenOnboarding.DomainEntities: Flow, Node, Connection, Session, Submission, CustomerProfile, Webhook. Enums: NodeType, ConditionOperator, SessionStatus.
ApplicationOpenOnboarding.ApplicationService interfaces (ports), request/response contracts, FluentValidation validators.
InfrastructureOpenOnboarding.InfrastructureWorkflowService, ComplianceRuleEvaluator, FlowService, WebhookService, EF Core DbContext, logic node executors.
APIOpenOnboarding.ApiASP.NET Core controllers, JWT + API key authentication, RBAC policies, OpenAPI.

The frontend is a Vite + React application. StepRenderer reads NodeDto.JsonContent and renders type-appropriate UI. JourneyBuilder provides a React Flow graph for visual path inspection. The Visual Journey Builder at /admin/journey-builder provides a drag-and-drop canvas for authoring flows without writing JSON.

Journey Schema

A flow is a JSON document describing nodes and their directed connections. The engine stores flows server-side and evaluates them at runtime for each session.

Flow object

FieldTypeDescription
idUUIDUnique identifier (server-assigned on creation).
namestringHuman-readable name for this journey.
descriptionstring?Optional description.
nodesNode[]Array of journey nodes.
connectionsConnection[]Array of directed edges between nodes.

Node object

FieldTypeDescription
idUUIDUnique node identifier within the flow.
keystringStable slug for referencing this node (e.g. in redirects or webhook payloads).
typeenumOne of: Form, DocumentUpload, Redirect, Information, Logic.
titlestring?Display heading shown by the renderer.
jsonContentstring?JSON string; schema depends on node type (see Node Types below).
complianceRuleJsonstring?Optional server-side validation rules (see Compliance Rules).
isStartNodebooleanExactly one node must be true. This is the entry point.

Connection object

FieldTypeDescription
sourceNodeIdUUIDThe node this edge originates from.
targetNodeIdUUIDThe node this edge points to.
conditionFieldstring?Name of the field to evaluate. Omit for an unconditional fallback edge.
conditionOperatorenum?One of the 12 supported operators (see Connections & Routing).
conditionValuestring?Value to compare against.
priorityintLower = evaluated first. Fallback edges (no condition) are always sorted to the end within the same priority.

journey.json Reference

In Waymark, journey.json is a flow definition with top-level metadata, nodes, and connections. The engine evaluates this graph at runtime for each session.

Notation

  • IDs are GUID strings for flow and node identity.
  • nodes[].jsonContent is JSON serialized as a string in the API contract.
  • nodes[].complianceRuleJson is optional JSON serialized as a string.
  • connections[].priority is evaluated ascending (lower first).
  • condition* fields can be omitted/null for fallback edges.

Minimal structure

journey.json
{
  "name": "Journey name",
  "description": "Optional description",
  "nodes": [],
  "connections": []
}

Frontend linkage

Journey JSON fieldFrontend usageResult
id (selected flow)App.tsx + useOnboardingPassed as StartSessionRequest.flowId to start a session.
nodes[].typeStepRenderer.tsxSelects rendered step UI (Form, DocumentUpload, Redirect, Information, Logic).
nodes[].jsonContentStepRenderer.tsxParsed into dynamic form/upload/redirect configuration.
connections[]Engine + JourneyBuilder.tsxControls branching and edge labels in the graph.
nodes[] / connections[]FlowAuthoringPanel.tsxEditable and validated as JSON in the authoring UI.

Journey visualization

The frontend visualizes journeys in three ways:

  • Runtime step rendering (StepRenderer) — the live UI a user sees during an onboarding session
  • Read-only graph (JourneyBuilder via React Flow) — monitors active session path progress
  • Visual Journey Builder canvas — interactive authoring UI at /admin/journey-builder
Step Renderer — schema-driven runtime UI for an active onboarding session
StepRenderer: schema-driven runtime UI rendered from jsonContent for a Form node
[country-form]
      ├── Country = USA ─────► [us-tax-form] ─────► [completed]
      └── Country != USA ────► [passport-upload] ─► [completed]

For a complete real example already included in this repository, see src/frontend/src/schemas/flow-definition.example.json .

Node Types

Form

Renders a dynamic form from a field list. Each field maps to an HTML input in the frontend StepRenderer.

jsonContent
{
  "fields": [
    { "name": "CompanyName", "type": "text",   "required": true },
    { "name": "Country",     "type": "select", "required": true, "options": ["USA", "GBR", "DEU"] },
    { "name": "Revenue",     "type": "number", "required": false }
  ]
}

Supported type values: text, email, number, select, checkbox, textarea, date. Unknown types fall back to text.

DocumentUpload

Renders a file picker. Files are uploaded via a separate endpoint before step submission.

jsonContent
{
  "acceptedFileTypes": ["application/pdf", "image/jpeg", "image/png"],
  "maxFiles": 3
}

Global file size limit is controlled by the DocumentUpload:MaxFileSizeBytes configuration key.

Redirect

Navigates the customer to an external URL. Supports {{token}} interpolation resolved server-side before delivery.

jsonContent
{
  "url": "https://kyc-provider.example.com/verify?session={{sessionId}}&customer={{externalCustomerId}}"
}
TokenResolves to
{{sessionId}}Current session GUID
{{flowId}}Flow GUID
{{nodeKey}}Node key string
{{customerProfileId}}Internal customer profile GUID
{{externalCustomerId}}Caller-supplied external customer ID
{{FieldName}}Any field value from the most recent submission

Information

Displays a message with no submission required. The node's title field carries the display text. No jsonContent is required.

Logic

Executes a server-side action automatically, without user interaction. The engine auto-advances through up to 20 consecutive Logic nodes before returning control to the caller.

jsonContent
{
  "action": "SetProfileField",
  "field": "kyc_status",
  "value": "pending",
  "failOnError": true
}
ActionDescriptionRequired fields
SetProfileFieldWrites a key-value pair into CustomerProfile.MetadataJson.field, value
HttpCallbackPOSTs the current step payload as JSON to an external URL and records the response.url

To add a custom action, implement ILogicNodeExecutor and register it with the DI container.

Connections & Routing

When a step is submitted, WorkflowService.ResolveNextNode selects the next node by evaluating outgoing connections in order:

  1. Connections are sorted ascending by priority (lower = evaluated first).
  2. Within the same priority, fallback connections (no conditionField) are sorted to the end.
  3. The first connection whose condition evaluates to true wins.
  4. If no connection matches, the session is marked Completed.

If conditionField is absent from the submitted payload, the engine falls back to matching Country and Email fields on the associated CustomerProfile.

All string comparisons are case-insensitive.

Supported condition operators

Equals NotEquals Exists Contains NotContains StartsWith EndsWith GreaterThan LessThan GreaterThanOrEqual LessThanOrEqual MatchesRegex

Numeric operators require both sides to parse as decimal; non-numeric values evaluate to false.

Compliance Rules

complianceRuleJson is an optional string on any node. The ComplianceRuleEvaluator service parses it at step-submission time. A non-empty violation list causes the submission to return HTTP 400.

complianceRuleJson shape
{
  "requiredFields": ["FieldA", "FieldB"],
  "rules": [
    { "field": "CompanyName", "minLength": 2, "maxLength": 100, "pattern": "^[A-Za-z0-9 ]+$" },
    { "field": "Revenue",     "minimum": 0,   "maximum": 999999999 },
    { "field": "RiskTier",    "allowedValues": ["Low", "Medium", "High"] }
  ],
  "crossFieldRules": [
    { "field1": "EndDate", "operator": "GreaterThan", "field2": "StartDate" }
  ]
}
SectionDescription
requiredFieldsField names that must be present and non-empty.
rules[].minLength / maxLengthString length constraints.
rules[].minimum / maximumNumeric range (parsed as decimal).
rules[].pattern.NET regex, 100 ms timeout.
rules[].allowedValuesCase-insensitive enumeration of permitted values.
crossFieldRulesCompares two fields. Numeric, date, or lexicographic comparison in order of precedence.

Session Lifecycle

StartSession ──► Started
                    │
           SubmitStep (compliance pass)
                    │
          ┌─────────▼──────────┐
          │  Logic auto-advance │  (up to 20 consecutive Logic nodes)
          └─────────┬──────────┘
                    │
        ┌───────────┼────────────┐
        │           │            │
   next node    no next node  failOnError
   resolved     resolved      triggered
        │           │            │
     Started    Completed      Error
        │
   AbandonSession
        │
    Abandoned
StatusDescription
StartedActive — awaiting next step submission.
CompletedTerminal — no outgoing connection matched from the last node.
AbandonedTerminal — explicitly set via DELETE /api/workflow/sessions/{id} (idempotent).
ErrorTerminal — a Logic node with failOnError: true threw, or 20 consecutive Logic nodes were auto-advanced.

SSE events are emitted on GET /api/workflow/sessions/{sessionId}/events for step-advanced, session-completed, and session-abandoned transitions.

Context & State

Waymark maintains two forms of state across a journey session:

Submission history

Every step submission is persisted as a Submission record. The engine can reference field values from any previous submission when evaluating cross-field compliance rules or connection conditions. Retrieve the full history at any time:

http
GET /api/workflow/sessions/{sessionId}

Customer profile metadata

CustomerProfile.MetadataJson is a free-form JSON bag that persists across sessions for a given customer. Logic nodes using SetProfileField can write into it. Connection conditions can read from it. Update it at any time via:

http
PUT /api/customers/{customerId}

Session resumption

Sessions persist server-side indefinitely. To resume a session after a page reload or app restart, persist the sessionId client-side (e.g. in localStorage) and call:

http
GET /api/workflow/sessions/{sessionId}/next

This returns the current node the session is waiting on, allowing seamless resumption.

Visual Journey Builder

The Visual Journey Builder is a drag-and-drop admin interface for designing and publishing onboarding flows without editing raw JSON. It is available at /admin/journey-builder and requires an authenticated session with the Operator role.

Visual Journey Builder — interactive canvas with node palette and properties panel
Visual Journey Builder: drag-and-drop canvas, node palette (top), and Properties Panel (right)

SSO required. Navigating to /admin/journey-builder while unauthenticated redirects to /login?returnUrl=…. After SSO, the original URL is restored automatically.

Canvas interactions

ActionHow
Move a nodeDrag it to a new position on the canvas
Create a connectionDrag from a node's bottom handle to another node's top handle
Select a node or edgeClick it — the Properties Panel opens on the right
Delete a node or edgePress Delete / Backspace, or click Delete node in the Properties Panel
Zoom / panScroll to zoom; drag the background to pan; use the Controls toolbar

Adding nodes

The palette above the canvas contains one button per node type. Clicking a button adds a new node at a default position with a generated GUID. Nodes are color-coded by type:

TypeColorPurpose
FormBlueDynamic form rendered from jsonContent field definitions
DocumentUploadPurpleFile picker with accepted types and maxFiles constraints
RedirectAmberExternal URL navigation with {{token}} interpolation
InformationGreenRead-only message; no submission required
LogicOrangeAutomatic server-side action (SetProfileField, HttpCallback, …)

Properties Panel

Selecting a node exposes its editable fields in a fixed right-hand panel:

  • Title — display label shown to the end user
  • Key — stable slug used in routing conditions and URL token interpolation
  • Type — dropdown; changing type does not clear jsonContent
  • Start node — checking this automatically clears the flag on every other node, enforcing exactly one start node per flow
  • jsonContent — type-specific configuration JSON (field list, upload constraints, redirect URL, Logic action)
  • Compliance rules — optional complianceRuleJson for server-side validation at submission time

Selecting an edge reveals its condition properties: conditionField, conditionOperator, conditionValue, and priority. Leave conditionField empty for a fallback (unconditional) edge.

Save actions

ButtonAPI callEffect
Load flowGET /api/flows/{id}Fetches an existing flow and hydrates the canvas
Create newPOST /api/flowsCreates the draft as a new flow; sets the returned ID in the panel
Save new versionPUT /api/flows/{id}Publishes the current canvas as the next version of the flow
ResetClears the canvas back to a single empty start node

Validation errors (missing flow name, invalid GUIDs, zero nodes, no start node, dangling connection references) are displayed inline before any network request is made.

Flow Authoring Panel

The Flow Authoring Panel is a power-user JSON authoring interface embedded in the developer dashboard. It exposes the same load / create / save / delete / verify operations as the Visual Journey Builder but works directly with raw JSON text areas for nodes and connections — useful for scripted testing and batch imports.

Both the Visual Journey Builder and the Flow Authoring Panel share the same underlying flowAuthoring.ts domain logic for validation (validateFlowDraft) and payload construction (buildFlowWritePayload), so validation rules are identical across both authoring surfaces.

Authentication

The API supports two authentication schemes. Requests are routed via a "Combined" policy scheme:

  • API Key — Pass X-Api-Key: <key> header. Local development key: dev-api-key-change-in-production.
  • JWT Bearer — Pass a signed JWT in the Authorization: Bearer <token> header.

Roles: Operator (full access), Applicant (session and step submission), ReadOnly (read-only operator endpoints).

Change the default API key before deploying to production. Set Authentication:ApiKey in application settings or as an environment variable.

Flows API

POST /api/flows Create a new flow
Creates a new journey flow with all its nodes and connections in a single request. Returns the created flow with server-assigned IDs.
GET /api/flows/{flowId} Get a flow
Returns the full flow definition including all nodes and connections.
PUT /api/flows/{flowId} Update a flow
Replaces the entire flow definition. All nodes and connections are replaced atomically. Active sessions continue on their previously resolved current node.

Sessions API

POST /api/workflow/sessions/start

Starts a new session for a given flow.

// Request body { "flowId": "uuid", "customerProfileId": "uuid?" }
GET /api/workflow/sessions/{sessionId}
Returns session details including status, current node, and full submission history.
GET /api/workflow/sessions/{sessionId}/next
Returns the current node the session is waiting on. Use for session resumption.
DELETE /api/workflow/sessions/{sessionId}
Abandons the session. Idempotent — safe to call multiple times.

Steps & Documents

POST /api/workflow/sessions/{sessionId}/steps/{nodeId}/submit

Submits a step. The engine runs compliance validation and resolves the next node.

// Request body { "payload": { "FieldName": "value", ... } }

Returns HTTP 200 with next node on success, HTTP 400 with violation list if compliance fails.

POST /api/workflow/sessions/{sessionId}/steps/{nodeId}/documents

Multipart upload for DocumentUpload nodes. Field name: files. Returns an array of StoredFileInfo:

[{ "fileId": "uuid", "fileName": "passport.pdf", "contentType": "application/pdf", "sizeBytes": 1024, "storedAt": "..." }]

Events & Webhooks

Server-Sent Events (SSE)

GET /api/workflow/sessions/{sessionId}/events
Opens an SSE stream. Events: step-advanced, session-completed, session-abandoned.

Webhook management

POST /api/flows/{flowId}/webhooks Register a callback URL
GET /api/flows/{flowId}/webhooks List registered webhooks
DELETE /api/flows/{flowId}/webhooks/{webhookId} Remove a webhook
GET /api/flows/{flowId}/webhook-deliveries View delivery history and retries

Webhook payloads include a X-Webhook-Signature header for HMAC verification. Failed deliveries are retried with exponential back-off.

React / Next.js Integration

The repository ships a reference useOnboarding hook that manages session state and SSE consumption. You can copy it directly or model your own integration after it.

Initialize and start a session

React
import { useState } from 'react'

const API = import.meta.env.VITE_API_BASE_URL
const KEY = import.meta.env.VITE_API_KEY

export function useWaymark(flowId: string) {
  const [sessionId, setSessionId] = useState<string | null>(null)
  const [currentNode, setCurrentNode] = useState<NodeDto | null>(null)
  const [status, setStatus] = useState<'idle' | 'active' | 'completed'>('idle')

  async function start() {
    const res = await fetch(`${API}/api/workflow/sessions/start`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY },
      body: JSON.stringify({ flowId }),
    })
    const data = await res.json()
    setSessionId(data.sessionId)
    setCurrentNode(data.currentNode)
    setStatus('active')
  }

  async function submit(nodeId: string, payload: Record<string, unknown>) {
    const res = await fetch(
      `${API}/api/workflow/sessions/${sessionId}/steps/${nodeId}/submit`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY },
        body: JSON.stringify({ payload }),
      }
    )
    const data = await res.json()
    if (data.nextNode) {
      setCurrentNode(data.nextNode)
    } else {
      setStatus('completed')
    }
  }

  return { start, submit, currentNode, status }
}

Render a node dynamically

React
import { useEffect } from 'react'

function NodeRenderer({ node, onSubmit }) {
  const content = node.jsonContent ? JSON.parse(node.jsonContent) : {}

  useEffect(() => {
    if (node.type === 'Redirect' && content.url) {
      window.location.assign(content.url)
    }
  }, [node.type, content.url])

  switch (node.type) {
    case 'Form':
      return <DynamicForm fields={content.fields} onSubmit={onSubmit} />
    case 'DocumentUpload':
      return <FileUpload accept={content.acceptedFileTypes} onSubmit={onSubmit} />
    case 'Redirect':
      return null
    case 'Information':
      return <p>{node.title}</p>
    default:
      return null
  }
}

Vue / Nuxt Integration

Vue 3 Composable
// composables/useWaymark.ts
import { ref } from 'vue'

export function useWaymark(flowId: string) {
  const sessionId  = ref<string | null>(null)
  const currentNode = ref<any | null>(null)
  const isComplete = ref(false)

  const API = import.meta.env.VITE_API_BASE_URL
  const KEY = import.meta.env.VITE_API_KEY

  async function start() {
    const response = await fetch(`${API}/api/workflow/sessions/start`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY },
      body: JSON.stringify({ flowId }),
    })
    const data = await response.json()
    sessionId.value   = data.sessionId
    currentNode.value = data.currentNode
  }

  async function submit(nodeId: string, payload: Record<string, unknown>) {
    const response = await fetch(
      `${API}/api/workflow/sessions/${sessionId.value}/steps/${nodeId}/submit`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY },
        body: JSON.stringify({ payload }),
      }
    )
    const data = await response.json()
    currentNode.value = data.nextNode ?? null
    if (!data.nextNode) isComplete.value = true
  }

  return { start, submit, currentNode, isComplete }
}

Node.js Integration

Node.js (fetch API)
const BASE = process.env.WAYMARK_API_URL
const KEY  = process.env.WAYMARK_API_KEY

async function startSession(flowId) {
  const res  = await fetch(`${BASE}/api/workflow/sessions/start`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY },
    body: JSON.stringify({ flowId }),
  })
  return res.json() // { sessionId, currentNode }
}

async function submitStep(sessionId, nodeId, payload) {
  const res = await fetch(
    `${BASE}/api/workflow/sessions/${sessionId}/steps/${nodeId}/submit`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Api-Key': KEY },
      body: JSON.stringify({ payload }),
    }
  )
  return res.json() // { nextNode } or null when complete
}

// Usage
const { sessionId, currentNode } = await startSession('my-flow-id')
const result = await submitStep(sessionId, currentNode.id, { Country: 'USA' })

Example: 3-Step Contact Form

contact-form.json
{
  "name": "Contact Form",
  "nodes": [
    {
      "id": "11111111-1111-1111-1111-111111111111", "key": "personal-info", "type": "Form",
      "title": "Your details", "isStartNode": true,
      "jsonContent": "{\"fields\":[{\"name\":\"Name\",\"type\":\"text\",\"required\":true},{\"name\":\"Email\",\"type\":\"email\",\"required\":true}]}",
      "complianceRuleJson": "{\"requiredFields\":[\"Name\",\"Email\"],\"rules\":[{\"field\":\"Email\",\"pattern\":\"^[^@]+@[^@]+\\\\.[^@]+$\"}]}"
    },
    {
      "id": "22222222-2222-2222-2222-222222222222", "key": "message", "type": "Form", "title": "Your message",
      "jsonContent": "{\"fields\":[{\"name\":\"Subject\",\"type\":\"text\",\"required\":true},{\"name\":\"Body\",\"type\":\"textarea\",\"required\":true}]}"
    },
    {
      "id": "33333333-3333-3333-3333-333333333333", "key": "confirmation", "type": "Information",
      "title": "Thank you! We'll be in touch within 24 hours."
    }
  ],
  "connections": [
    { "sourceNodeId": "11111111-1111-1111-1111-111111111111", "targetNodeId": "22222222-2222-2222-2222-222222222222" },
    { "sourceNodeId": "22222222-2222-2222-2222-222222222222", "targetNodeId": "33333333-3333-3333-3333-333333333333" }
  ]
}

Example: Conditional Payment Flow

Routes to credit card validation or PayPal based on the customer's selection. Compliance rules enforce card number format.

payment.json
{
  "name": "Conditional Payment",
  "nodes": [
    {
      "id": "11111111-1111-1111-1111-111111111111", "key": "select-method", "type": "Form",
      "title": "How would you like to pay?", "isStartNode": true,
      "jsonContent": "{\"fields\":[{\"name\":\"Method\",\"type\":\"select\",\"options\":[\"CreditCard\",\"PayPal\"],\"required\":true}]}"
    },
    {
      "id": "22222222-2222-2222-2222-222222222222", "key": "card-details", "type": "Form",
      "title": "Enter card details",
      "jsonContent": "{\"fields\":[{\"name\":\"CardNumber\",\"type\":\"text\",\"required\":true},{\"name\":\"CVV\",\"type\":\"text\",\"required\":true},{\"name\":\"Expiry\",\"type\":\"text\",\"required\":true}]}",
      "complianceRuleJson": "{\"requiredFields\":[\"CardNumber\",\"CVV\"],\"rules\":[{\"field\":\"CardNumber\",\"pattern\":\"^[0-9]{16}$\"},{\"field\":\"CVV\",\"pattern\":\"^[0-9]{3,4}$\"}]}"
    },
    {
      "id": "33333333-3333-3333-3333-333333333333", "key": "paypal", "type": "Redirect",
      "title": "Redirecting to PayPal…",
      "jsonContent": "{\"url\":\"https://paypal.com/checkout?session={{sessionId}}\"}"
    },
    { "id": "44444444-4444-4444-4444-444444444444", "key": "success", "type": "Information", "title": "Payment accepted!" }
  ],
  "connections": [
    { "sourceNodeId": "11111111-1111-1111-1111-111111111111", "targetNodeId": "22222222-2222-2222-2222-222222222222", "conditionField": "Method", "conditionOperator": "Equals", "conditionValue": "CreditCard", "priority": 0 },
    { "sourceNodeId": "11111111-1111-1111-1111-111111111111", "targetNodeId": "33333333-3333-3333-3333-333333333333", "priority": 1 },
    { "sourceNodeId": "22222222-2222-2222-2222-222222222222", "targetNodeId": "44444444-4444-4444-4444-444444444444" },
    { "sourceNodeId": "33333333-3333-3333-3333-333333333333", "targetNodeId": "44444444-4444-4444-4444-444444444444" }
  ]
}

Example: Enterprise Onboarding

Admin users see a full KYC flow with document upload; guest users are directed to a lightweight information step.

enterprise-onboarding.json
{
  "name": "Enterprise Onboarding",
  "nodes": [
    {
      "id": "11111111-1111-1111-1111-111111111111", "key": "user-type", "type": "Form",
      "title": "Tell us about yourself", "isStartNode": true,
      "jsonContent": "{\"fields\":[{\"name\":\"Role\",\"type\":\"select\",\"options\":[\"Admin\",\"Guest\"],\"required\":true},{\"name\":\"Company\",\"type\":\"text\",\"required\":true}]}"
    },
    {
      "id": "22222222-2222-2222-2222-222222222222", "key": "admin-kyc", "type": "Form", "title": "Identity verification",
      "jsonContent": "{\"fields\":[{\"name\":\"TaxId\",\"type\":\"text\",\"required\":true},{\"name\":\"Jurisdiction\",\"type\":\"select\",\"options\":[\"USA\",\"GBR\",\"DEU\"],\"required\":true}]}",
      "complianceRuleJson": "{\"requiredFields\":[\"TaxId\",\"Jurisdiction\"]}"
    },
    {
      "id": "33333333-3333-3333-3333-333333333333", "key": "admin-docs", "type": "DocumentUpload",
      "title": "Upload your company registration document",
      "jsonContent": "{\"acceptedFileTypes\":[\"application/pdf\"],\"maxFiles\":1}"
    },
    {
      "id": "44444444-4444-4444-4444-444444444444", "key": "flag-for-review", "type": "Logic",
      "jsonContent": "{\"action\":\"SetProfileField\",\"field\":\"kyc_status\",\"value\":\"pending_review\",\"failOnError\":false}"
    },
    { "id": "55555555-5555-5555-5555-555555555555", "key": "guest-welcome", "type": "Information", "title": "Welcome! Your account has been created." },
    { "id": "66666666-6666-6666-6666-666666666666", "key": "admin-complete", "type": "Information", "title": "KYC submitted — we'll review and notify you within 2 business days." }
  ],
  "connections": [
    { "sourceNodeId": "11111111-1111-1111-1111-111111111111", "targetNodeId": "22222222-2222-2222-2222-222222222222", "conditionField": "Role", "conditionOperator": "Equals", "conditionValue": "Admin", "priority": 0 },
    { "sourceNodeId": "11111111-1111-1111-1111-111111111111", "targetNodeId": "55555555-5555-5555-5555-555555555555", "priority": 1 },
    { "sourceNodeId": "22222222-2222-2222-2222-222222222222", "targetNodeId": "33333333-3333-3333-3333-333333333333" },
    { "sourceNodeId": "33333333-3333-3333-3333-333333333333", "targetNodeId": "44444444-4444-4444-4444-444444444444" },
    { "sourceNodeId": "44444444-4444-4444-4444-444444444444", "targetNodeId": "66666666-6666-6666-6666-666666666666" }
  ]
}