npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@habit.analytics/habit-claims-journey-components

v2.4.0

Published

Visual graph-based journey builder and step-by-step form runner React widgets for claims workflows

Readme

Journey Builder & Form Runner

A linear, step-based form configurator and a self-contained form runner, distributed as two embeddable React widgets.

  • Builder (JourneyBuilderWidget) — configure a journey as ordered Steps → Rows → Fields, define per-step navigation rules (JSON Logic) and optional API calls, plus a Summary panel and Init values. Outputs a portable JourneySpec (v2.0).
  • Runner (FormRunnerWidget) — feeds that JourneySpec to a step-by-step engine that evaluates conditions, optionally calls APIs between steps (delegated to the host), collects answers, and renders a review/summary screen.

v2.0 — breaking change vs v1.x. The visual graph canvas (React Flow / @xyflow/react) has been removed. Legacy v1.x journeys (nodes/edges) are still accepted and auto-converted on import for read-only consumption, but new journeys are authored against the form_config model described below.


Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Host Application                        │
│                                                             │
│  ┌──────────────────────────┐   ┌────────────────────────┐  │
│  │  JourneyBuilderWidget    │   │   FormRunnerWidget     │  │
│  │                          │   │                        │  │
│  │  specs    ──► Catalog    │   │  journey ──► Engine    │  │
│  │  journey  ──► Editor     │   │  initialAnswers ──►    │  │
│  │              Steps/Rows  │   │              prefill    │  │
│  │              Fields tab  │   │  onStepApiCall ◄────   │  │
│  │              Init / JSON │   │  onStepApiError ◄────  │  │
│  │              Summary     │   │  onFileSelect ◄────    │  │
│  │  onSave / onPublish ──►  │   │  onSubmit(answers) ──► │  │
│  │  onChange ──►            │   │  onSave (draft) ──►    │  │
│  └──────────────────────────┘   └────────────────────────┘  │
│                                                             │
│       Both consume `JourneySpec` + types from               │
│                  `src/types/journey.ts`                     │
└─────────────────────────────────────────────────────────────┘

Tech Stack

| Layer | Technology | |---|---| | UI framework | React 18 + TypeScript | | State management | Zustand (scoped per widget instance, 50-step undo/redo, dirty tracking) | | Condition engine | json-logic-js | | Styling | Tailwind CSS (prefixed cj-) + scoped CSS variables (--claims-journey-*) | | Build tool | Vite |

Peer deps: react, react-dom. The previous @xyflow/react peer dep has been removed in v2.0.


Quick Start

npm install
npm run dev

Demo app at http://localhost:5173:

  • / — Builder with sample specs
  • /runner — Runner loaded from the builder's current journey

Installation (as a library)

npm install habit-claims-journey-components
import {
  JourneyBuilderWidget,
  FormRunnerWidget,
} from 'habit-claims-journey-components';

import type {
  JourneySpec,
  ClaimPropertySpec,
  FormConfig,
  EnrichedAnswer,
  StepApiRequest,
  InitApiRequest,
} from 'habit-claims-journey-components';

CSS is auto-injected — do not import a separate style.css.

The parent of JourneyBuilderWidget must have an explicit height (the widget fills 100%).

See INTEGRATION.md for full install methods and copy-paste setup.


Usage

JourneyBuilderWidget

import { JourneyBuilderWidget } from 'habit-claims-journey-components';
import type { ClaimPropertySpec, JourneySpec } from 'habit-claims-journey-components';

function BuilderPage({
  specs,
  existing,
}: {
  specs: ClaimPropertySpec[];
  existing?: JourneySpec | null;
}) {
  return (
    <div style={{ width: '100%', height: '100vh' }}>
      <JourneyBuilderWidget
        specs={specs}
        journey={existing}
        onSave={(spec) => saveDraft(spec)}
        onPublish={(spec) => publish(spec)}
        onChange={(spec) => console.log('changed', spec)}
        onOpenRunner={() => navigate('/runner')}
        readOnly={false}
        googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
      />
    </div>
  );
}

Props

| Prop | Type | Required | Description | |---|---|---|---| | specs | ClaimPropertySpec[] | — | Field specifications shown in the catalog and "Add field" dialogs | | journey | JourneySpec \| null | — | Existing journey to load on mount. v1.x graph and v2.x form_config both accepted | | onSave | (spec: JourneySpec) => void | — | Fired by the Save button. Marks the store as clean | | onPublish | (spec: JourneySpec) => void | — | Fired by the Publish button | | onChange | (spec: JourneySpec) => void | — | Fired on every config mutation (steps/rows/fields/navigation/summary/init/theme) | | onUnlockEdit | () => void | — | Shown as "Unlock edit" when readOnly is true | | onOpenRunner | () => void | — | Shown as a "Preview" button in the header | | readOnly | boolean | — | Disables editing, hides destructive controls, surfaces the unlock action | | googleMapsApiKey | string | — | Enables the Google Places address widget in previews |

The builder UI exposes four tabs: Steps, Fields, Init, JSON.


FormRunnerWidget

import { FormRunnerWidget } from 'habit-claims-journey-components';
import type { JourneySpec, StepApiRequest } from 'habit-claims-journey-components';

function RunnerPage({ journey }: { journey: JourneySpec }) {
  return (
    <FormRunnerWidget
      journey={journey}
      initialAnswers={{ /* optional pre-fill */ }}
      enrichedOutput
      onSubmit={(answers) => submitToApi(answers)}
      onSave={(partial) => saveDraft(partial)}
      onFileSelect={async (file, key) => uploadToS3(file, key)}
      onStepApiCall={async (req: StepApiRequest) => {
        const res = await fetch(req.url, {
          method: req.method,
          headers: { 'Content-Type': 'application/json', ...req.headers },
          body: req.body ? JSON.stringify(req.body) : undefined,
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      }}
      onStepApiError={(err, req) => trackError(err, req)}
      onInitApiCall={async (req) => (await fetch(req.url, { method: req.method, headers: req.headers })).json()}
      googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
      fullScreen
    />
  );
}

Props

| Prop | Type | Required | Description | |---|---|---|---| | journey | JourneySpec | ✅ | The journey to run. v1.x or v2.x | | onSubmit | (answers: Record<string, unknown>) => void | — | Called when the user confirms the summary screen. Defaults to copying JSON to clipboard | | onSave | (partial: Record<string, unknown>) => void | — | Called whenever the user reaches a checkpoint mid-journey, suitable for draft persistence | | onFileSelect | (file: File, questionKey: string) => Promise<string \| UploadResult \| void> \| void | — | Host-side upload handler for upload widgets. Returns a URL or { url, name, ... } | | onStepApiCall | (req: StepApiRequest) => Promise<unknown> | — | Recommended. Delegates per-step API calls to the host (auth, CORS, retries, telemetry). Return value is stored as that step's api_response and is referenceable from later JSON Logic and the Summary | | onStepApiError | (err: unknown, req: StepApiRequest) => void | — | Called when a step API call fails. When provided, the runner skips its default error toast. Fires regardless of the on_error policy. Exceptions thrown by this handler are caught and logged — they never crash the runner | | onInitApiCall | (req: InitApiRequest) => Promise<unknown> | — | Delegates the optional init/prefill API call to the host | | initialAnswers | Record<string, unknown> | — | Seeds the runner with existing values. The engine walks forward until it hits the first unanswered question, or straight to the summary if everything is filled | | enrichedOutput | boolean | — | When true, onSubmit receives { [key]: { data, schema } } instead of the flat answers map | | googleMapsApiKey | string | — | Enables the Google Places address widget | | fullScreen | boolean | — | Renders the runner full-viewport (otherwise it fills its parent) |

StepApiRequest
type StepApiRequest = {
  step_id: string;
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  url: string;
  headers: Record<string, string>;
  body?: unknown;                          // already-interpolated JSON
  answers: Record<string, unknown>;        // current answers snapshot
};
InitApiRequest
type InitApiRequest = {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  url: string;
  headers?: Record<string, string>;
};
Step API on_error policy

Each step's navigation.api_call.on_error controls what happens when the call (or onStepApiCall) throws:

| Value | Behavior | |---|---| | block (default) | Stay on the current step, surface an error. apiResponses[step_id] is left untouched (preserves any previous successful response) | | continue | Proceed using the step's navigation rules. api_response for this step stays unset; any previous successful value is preserved | | goto:<step_id> | Jump straight to the given step (e.g. an error screen), bypassing rules |

onStepApiError is always called when present, regardless of policy. If the handler itself throws, the runner catches the error, logs it (onStepApiError handler threw), and still honors the configured on_error policy.


Data Model — JourneySpec (v2.0)

A JourneySpec is a portable envelope that wraps a form_config plus journey-level metadata.

interface JourneySpec {
  journey_id: string;
  claimspec_id: string;
  version: string;             // "2.0.x" for the linear model
  form_config: FormConfig;     // ◀ the new authoring model
  theme?: JourneyTheme;
  mode?: JourneyMode;

  // Legacy v1.x — preserved when re-exporting an imported v1 spec.
  nodes?: JourneySpecNode[];
  edges?: JourneySpecEdge[];
  start_node_id?: string | null;
}

FormConfig

interface FormConfig {
  steps: FormStep[];               // ordered
  fields: FormField[];             // flat catalog, referenced by id
  summary: SummaryPanelConfig;
  init: InitConfig;
}

FormStep

interface FormStep {
  step_id: string;
  title: string;
  description?: string;
  rows: FormRow[];                 // each row has field_ids[]
  navigation: {
    api_call?: StepApiCall;        // fired when leaving the step
    rules: NavRule[];              // JSON Logic; first match wins
    button_label?: string;
  };
}

NavRule

interface NavRule {
  rule_id: string;
  condition: Record<string, unknown> | null;   // null = always (default)
  next_step_id: string;                        // or "__submit__"
  priority?: number;
}

StepApiCall

interface StepApiCall {
  enabled: boolean;
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  url: string;
  headers?: Record<string, string>;
  body_template?: string;          // tokens like {{answers.foo}} interpolated by the runner
  on_error?: 'block' | 'continue' | `goto:${string}`;   // defaults to 'block'
}

FormField

interface FormField {
  field_id: string;
  namespace: string;               // answer key
  label: string;
  labelOverride?: string;
  specId?: string;                 // or "custom"
  schema: string;                  // e.g. "v1_string", "v2_date-1"
  options?: ClaimPropertySpecOption[] | null;
  widget?: string;                 // override (e.g. "email", "textarea")
  required?: boolean;
  actor?: 'customer' | 'operator';
  bindings?: { target_namespace: string }[];
  uploadConfig?: UploadConfig;
  allowFuture?: boolean;
  placeholder?: string;
  helpText?: string;
}

SummaryPanelConfig

interface SummaryPanelConfig {
  title: string;
  show_pricing?: boolean;
  show_sidebar?: boolean; // defaults to true
  sections: SummarySection[];
}

interface SummarySection {
  section_id: string;
  title: string;
  visible_from_step?: number;      // 1-based
  until_step?: number;             // 1-based
  display_fields: SummaryDisplayField[];
}

interface SummaryDisplayField {
  display_id: string;
  source: 'form_field' | 'api_response';
  field_id?: string;               // when source === 'form_field'
  step_id?: string;                // when source === 'api_response'
  response_path?: string;          // dot-path inside api_response
  label: string;
  format?: 'text' | 'date' | 'datetime' | 'currency' | 'number';
}

InitConfig

interface InitConfig {
  values: Record<string, unknown>;
  api?: {
    enabled: boolean;
    method: 'GET' | 'POST';
    url: string;
    headers?: Record<string, string>;
    mapping?: Record<string, string>;   // namespace -> response_path
  };
}

JourneyTheme

CSS variables injected on the runner/builder root, overriding --claims-journey-* defaults. Colors are HSL triplets (e.g. "222 47% 11%").

interface JourneyTheme {
  primaryColor?: string;
  backgroundColor?: string;
  cardColor?: string;
  foregroundColor?: string;
  fontFamilyHeading?: string;
  fontFamilyBody?: string;
  fontSizeBase?: number;     // px
  borderRadius?: number;     // rem
  spacing?: 'compact' | 'cozy' | 'spacious';
}

JSON Schema validation

Draft-07 JSON Schemas describing FormConfig and JourneySpec v2 ship with the package and are re-exported as plain objects:

import Ajv from 'ajv';
import { formConfigSchema, journeySpecSchema } from 'habit-claims-journey-components';

const ajv = new Ajv({ allErrors: true, strict: false });
ajv.addSchema(formConfigSchema);          // resolved by $id from journeySpecSchema
const validate = ajv.compile(journeySpecSchema);

if (!validate(spec)) console.error(validate.errors);

Schema sources:

See docs/SCHEMAS.md for the full reference of required fields, types, and enumerations.


Supported Schema Types

| Schema | Widget | Validation | |---|---|---| | v1_string | Text input | — | | v2_single_option_select-1 | Dropdown select | Must pick an option | | v2_date-1 | Date picker | Valid date; allowFuture controls future dates | | v2_currency-1 | Currency input | Numeric | | v2_single_asset_upload | File upload (via onFileSelect) | Type/size from uploadConfig | | v2_phone-1 | Phone with country code | Length check | | v2_email-1 | Email input | Format regex |

Upload Configuration (uploadConfig)

interface UploadConfig {
  accept?: string;           // MIME types or extensions (e.g. "image/*,.pdf")
  maxSizeMB?: number;        // Max file size in MB
  multiple?: boolean;        // When true, allows multiple files
  maxFiles?: number;         // Max number of files (when multiple is true)
}

Single-file upload (default): uploadConfig without multiple or with multiple: false. Multi-file upload: set uploadConfig.multiple: true. When maxFiles is set, the widget enforces the limit.

Multi-File Upload Example

{
  "field_id": "f-photos",
  "namespace": "claim.photos",
  "label": "Photos",
  "schema": "v2_single_asset_upload",
  "widget": "upload",
  "uploadConfig": {
    "accept": "image/*,.pdf",
    "maxSizeMB": 10,
    "multiple": true,
    "maxFiles": 5
  }
}

When multiple: true, the answer value in onSubmit is an array of UploadResult:

{
  "claim.photos": [
    { "filename": "photo1.jpg", "url": "https://...", "mimetype": "image/jpeg", "size": 12345 },
    { "filename": "photo2.jpg", "url": "https://...", "mimetype": "image/jpeg", "size": 67890 }
  ]
}

See docs/MULTI-UPLOAD.md for a full migration guide.

Fields can override the default widget via widget (e.g. a v1_string rendered as "textarea" or "email").


Builder Features

  • Steps tab — three-pane layout (Steps list / Step editor / Summary editor) with drag-and-drop rows and inline field reuse from the catalog
  • Fields tab — global catalog with Used/Unused filter and quick-jump chips that take you straight to the Step using each field
  • Init tab — static initial values + optional prefill API call mapping (namespace → response_path)
  • JSON tab — full JourneySpec view with copy / download / paste-to-import escape hatch
  • Per-step API calls — method, URL, headers, JSON body template ({{answers.foo}}), and on_error policy
  • Per-step navigation rules — JSON Logic conditions, priority, __submit__ target
  • Theme panel — primary/background/card/foreground colors, fonts, base size, radius, spacing scale
  • Persistence — dirty-state tracking, Save / Publish, 50-action undo/redo, read-only mode with unlock action

Runner Features

  • Step-by-step navigation with non-destructive back/forward and mid-journey draft persistence (via initialAnswers)
  • Auto-progression through steps whose only effect is a navigation rule
  • JSON Logic evaluation against { answers, api_responses }
  • Host-delegated APIs — per-step (onStepApiCall) and init (onInitApiCall), with three on_error policies
  • File upload — async onFileSelect returning a URL or rich UploadResult
  • Schema-aware widgets with mobile-first touch targets (48px min) and iOS-zoom-safe inputs
  • Vertical summary screen rendering the configured SummaryPanelConfig
  • Enriched outputenrichedOutput flag wraps each answer as { data, schema }

Project Structure

src/
├── index.ts                                # Barrel exports
├── types/journey.ts                        # Public types & helpers
├── components/
│   ├── form-configurator/                  # ◀ v2.0 Builder
│   │   ├── JourneyBuilderWidget.tsx        # Entry point
│   │   ├── FormConfigurator.tsx            # Tabs + layout
│   │   ├── steps/                          # Steps tab
│   │   ├── fields/                         # Fields tab (Used/Unused filter)
│   │   ├── init/                           # Init tab
│   │   ├── json/                           # JSON tab
│   │   └── summary/                        # Summary editor
│   ├── runner/
│   │   └── FormRunnerWidget.tsx            # ◀ Runner entry point
│   ├── journey-builder/                    # Shared previews, theme panel, portal context
│   │   └── preview-widgets/                # Schema-specific widgets
│   └── ui/                                 # shadcn/ui primitives
├── stores/
│   └── formConfiguratorStore.ts            # Zustand store + legacy converter
├── lib/
│   └── theme-css.ts                        # CSS-variable builder for JourneyTheme
└── pages/                                  # Demo app
    ├── Index.tsx
    └── Runner.tsx

Migration v1 → v2

v2.0 replaces the React Flow graph canvas with a linear Steps → Rows → Fields configurator. Legacy v1.x specs (nodes/edges/start_node_id) are still accepted on import and auto-converted to a form_config.

Removed dependency

- "@xyflow/react": "^12.x"   // no longer a peer dependency

CSS

- import 'habit-claims-journey-components/style.css';
+ // CSS is auto-injected by the bundle — no separate import needed

JourneyBuilderWidget props

| v1.x | v2.x | Notes | |---|---|---| | specs | specs | Unchanged | | journey | journey | Accepts both v1 and v2 shapes | | onSave(spec) | onSave(spec) | Now also marks the store as clean | | — | onPublish(spec) | New — dedicated publish action | | — | onChange(spec) | New — fires on every mutation | | — | onOpenRunner() | New — header "Preview" button | | — | readOnly | New — disables editing | | — | onUnlockEdit() | New — shown when readOnly is on | | — | googleMapsApiKey | New — enables Location widget previews | | onNodeClick, onEdgeClick, nodeTypes, edgeTypes | — | Removed (no canvas) |

  <JourneyBuilderWidget
    specs={specs}
    journey={existing}
    onSave={spec => save(spec)}
+   onPublish={spec => publish(spec)}
+   onChange={spec => setDraft(spec)}
+   onOpenRunner={() => navigate('/runner')}
+   readOnly={false}
+   googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAPS_KEY}
  />

FormRunnerWidget props

| v1.x | v2.x | Notes | |---|---|---| | journey | journey | Now drives form_config (or auto-converted legacy graph) | | onSubmit(answers) | onSubmit(answers) | Set enrichedOutput for { data, schema } shape | | onFileSelect(file, key) | onFileSelect(file, key) | May now return UploadResult { url, filename, mimetype?, size? } | | onApiCall(req) | onStepApiCall(req: StepApiRequest) | Renamed; request includes step_id and answers snapshot | | — | onStepApiError(err, req) | New — suppresses the default error toast; safe even if it throws | | — | onInitApiCall(req: InitApiRequest) | New — host-delegated init/prefill call | | — | onSave(partial) | New — mid-journey draft persistence | | initialValues | initialAnswers | Renamed for consistency with the engine | | — | enrichedOutput | New — wraps every answer as { data, schema } | | — | fullScreen | New — viewport-fill layout | | mode, groups | — | Ignored; layout is now driven by form_config.steps/rows |

  <FormRunnerWidget
    journey={journey}
-   initialValues={draft}
+   initialAnswers={draft}
+   enrichedOutput
-   onApiCall={async (req) => fetchJson(req.url, req)}
+   onStepApiCall={async (req) => fetchJson(req.url, req)}
+   onStepApiError={(err, req) => logger.warn(err, req)}
+   onInitApiCall={async (req) => fetchJson(req.url, req)}
+   onSave={(partial) => saveDraft(partial)}
    onSubmit={(answers) => submit(answers)}
  />

JourneySpec shape

  {
    "journey_id": "j-001",
    "claimspec_id": "cs-001",
-   "version": "1.0",
-   "nodes": [ /* JourneySpecNode[] */ ],
-   "edges": [ /* JourneySpecEdge[] */ ],
-   "start_node_id": "node-1",
-   "mode": "sequential",
-   "groups": []
+   "version": "2.0",
+   "form_config": {
+     "steps":   [ /* FormStep[]  — rows -> field_ids */ ],
+     "fields":  [ /* FormField[] — flat catalog */ ],
+     "summary": { "title": "Review", "sections": [] },
+     "init":    { "values": {} }
+   }
  }

Importing a v1 spec is transparent: the store converts nodes/edges to form_config on load. Re-exporting preserves the legacy fields for downstream consumers that still read them.

Step API errors

v1 always blocked navigation on a failing API call. v2 introduces an explicit per-step policy on navigation.api_call.on_error:

| Value | Behavior | |---|---| | block (default) | Stay on the step; apiResponses[step_id] is preserved | | continue | Run navigation rules; api_response for this step stays unset | | goto:<step_id> | Jump to the named step (e.g. an error screen) |


Full Examples

Minimal FormConfig

A two-step journey with a select that branches to a follow-up step or skips to submit.

{
  "steps": [
    {
      "step_id": "step-vehicle",
      "title": "Your vehicle",
      "rows": [
        { "row_id": "r1", "layout": "vertical", "field_ids": ["f-plate", "f-has-damage"] }
      ],
      "navigation": {
        "rules": [
          {
            "rule_id": "rule-damage",
            "condition": { "==": [{ "var": "answers.vehicle.has_damage" }, "yes"] },
            "next_step_id": "step-damage",
            "priority": 10
          },
          { "rule_id": "rule-default", "condition": null, "next_step_id": "__submit__" }
        ]
      }
    },
    {
      "step_id": "step-damage",
      "title": "Describe the damage",
      "rows": [{ "row_id": "r2", "layout": "vertical", "field_ids": ["f-damage-notes"] }],
      "navigation": {
        "rules": [{ "rule_id": "r-end", "condition": null, "next_step_id": "__submit__" }]
      }
    }
  ],
  "fields": [
    { "field_id": "f-plate", "namespace": "vehicle.plate", "label": "License plate", "schema": "v1_string", "required": true },
    {
      "field_id": "f-has-damage",
      "namespace": "vehicle.has_damage",
      "label": "Is the vehicle damaged?",
      "schema": "v2_single_option_select-1",
      "required": true,
      "options": [
        { "data": "yes", "label": "Yes" },
        { "data": "no",  "label": "No" }
      ]
    },
    { "field_id": "f-damage-notes", "namespace": "vehicle.damage_notes", "label": "Damage description", "schema": "v1_string", "widget": "textarea" }
  ],
  "summary": {
    "title": "Review your claim",
    "sections": [
      {
        "section_id": "sec-vehicle",
        "title": "Vehicle",
        "display_fields": [
          { "display_id": "d-plate",  "source": "form_field", "field_id": "f-plate",        "label": "Plate" },
          { "display_id": "d-damage", "source": "form_field", "field_id": "f-damage-notes", "label": "Damage" }
        ]
      }
    ]
  },
  "init": { "values": { "vehicle.country": "BR" } }
}

Full JourneySpec with API call + init prefill

Per-step api_call with on_error: "goto:...", a summary section reading from api_response, and an init.api prefill.

{
  "journey_id": "j-auto-claim-001",
  "claimspec_id": "cs-auto-1",
  "version": "2.0",
  "form_config": {
    "steps": [
      {
        "step_id": "step-id",
        "title": "Find your policy",
        "rows": [{ "row_id": "r1", "layout": "horizontal", "field_ids": ["f-doc"] }],
        "navigation": {
          "api_call": {
            "enabled": true,
            "method": "POST",
            "url": "https://api.example.com/policies/lookup",
            "headers": { "x-tenant": "acme" },
            "body_template": "{\"document\":\"{{answers.customer.document}}\"}",
            "on_error": "goto:step-not-found"
          },
          "rules": [
            { "rule_id": "r-ok", "condition": null, "next_step_id": "step-confirm" }
          ]
        }
      },
      {
        "step_id": "step-confirm",
        "title": "Confirm your policy",
        "rows": [{ "row_id": "r2", "layout": "vertical", "field_ids": ["f-accept"] }],
        "navigation": {
          "rules": [{ "rule_id": "r-end", "condition": null, "next_step_id": "__submit__" }]
        }
      },
      {
        "step_id": "step-not-found",
        "title": "We couldn't find your policy",
        "rows": [],
        "navigation": { "rules": [] }
      }
    ],
    "fields": [
      { "field_id": "f-doc",    "namespace": "customer.document", "label": "Document ID", "schema": "v1_string", "required": true },
      { "field_id": "f-accept", "namespace": "consent.accepted",  "label": "I confirm the details above", "schema": "v2_single_option_select-1", "required": true,
        "options": [{ "data": "yes", "label": "Yes" }] }
    ],
    "summary": {
      "title": "Review",
      "sections": [
        {
          "section_id": "sec-policy",
          "title": "Policy",
          "display_fields": [
            { "display_id": "d-holder", "source": "api_response", "step_id": "step-id", "response_path": "policy.holder.name",   "label": "Holder" },
            { "display_id": "d-plate",  "source": "api_response", "step_id": "step-id", "response_path": "policy.vehicle.plate", "label": "Plate" }
          ]
        }
      ]
    },
    "init": {
      "values": {},
      "api": {
        "enabled": true,
        "method": "GET",
        "url": "https://api.example.com/session/prefill",
        "headers": { "x-tenant": "acme" },
        "mapping": {
          "customer.full_name": "user.name",
          "customer.email":     "user.email"
        }
      }
    }
  },
  "theme": {
    "primaryColor": "222 47% 11%",
    "fontFamilyHeading": "Poppins, sans-serif",
    "fontFamilyBody": "Roboto, sans-serif",
    "borderRadius": 0.75,
    "spacing": "cozy"
  }
}

Handling StepApiRequest on the host

The runner builds a StepApiRequest whenever a step has an enabled api_call. body is already interpolated from body_template against the live answers; answers is a snapshot for host-side enrichment (auth, tenant context, etc.).

import type { StepApiRequest } from 'habit-claims-journey-components';

async function onStepApiCall(req: StepApiRequest) {
  // req = { step_id, method, url, headers, body?, answers }
  const res = await fetch(req.url, {
    method: req.method,
    headers: {
      'Content-Type': 'application/json',
      authorization: `Bearer ${getToken()}`,
      ...req.headers,
    },
    body: req.body !== undefined ? JSON.stringify(req.body) : undefined,
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  // The returned value is stored as api_responses[req.step_id]
  // and is referenceable from later JSON Logic rules and the Summary.
  return res.json();
}

Handling InitApiRequest on the host

Fired once on runner mount when form_config.init.api.enabled is true. The response is walked through init.api.mapping (namespace → dot-path) to seed answers before the first step renders.

import type { InitApiRequest } from 'habit-claims-journey-components';

async function onInitApiCall(req: InitApiRequest) {
  // req = { method, url, headers? }
  const res = await fetch(req.url, { method: req.method, headers: req.headers });
  if (!res.ok) throw new Error(`Init HTTP ${res.status}`);
  return res.json();
}

License

MIT