Skip to main content
The workflow service drives interactive customer flows (retention prompts, dunning, onboarding). It speaks to a backend execution engine via a request/response API and exposes the running flow as reactive state — current step, available responses, completion outcome.

Setup

import {
  provideContext,
  createWorkflowService,
  createWorkflowApiAdapter,
  createMockWorkflowApiAdapter,
  WorkflowServiceContext,
} from "@juo/blocks";

provideContext(
  root,
  WorkflowServiceContext,
  createWorkflowService({
    adapter: createWorkflowApiAdapter(sdk),
    // ...other dependencies (translations, theme state)
  }),
);
For local development, swap the adapter:
createWorkflowService({ adapter: createMockWorkflowApiAdapter() });

Shape (key surface)

type WorkflowService = {
  // Reactive state
  isActive: ReadonlySignal<boolean>;
  isLoading: ReadonlySignal<boolean>;
  isCompleted: ReadonlySignal<boolean>;
  hasError: ReadonlySignal<boolean>;
  canRespond: ReadonlySignal<boolean>;
  currentStepInfo: ReadonlySignal<WorkflowStepInfo | null>;
  currentInteraction: ReadonlySignal<PendingInteraction | null>;

  // Lifecycle
  startFlow(flowType: string, params?: Record<string, unknown>): Promise<void>;
  respond(responseType: string, data?: Record<string, unknown>): Promise<void>;
  cancel(): Promise<void>;
};
WorkflowStepInfo carries the current step’s actionType, actionConfig, and the list of availableResponses the customer can pick from.

Execution model

Workflows are deterministic request/response — no websockets, no in-memory timers. Each customer action becomes a respond() call; the backend advances the state machine and returns the next step. Timeouts are orchestrator-driven (the backend schedules them).

Example: workflow action block

A typical action block reads the current step config and offers a response button per available choice:
import {
  defineBlock,
  effect,
  untracked,
  injectContext,
  WorkflowServiceContext,
} from "@juo/blocks";
import { html, render } from "lit-html";

defineBlock("DiscountOffer", {
  group: "action",
  schema: { /* ... */ } as const,
  initialValue: () => ({ props: {} }),
  renderer(block) {
    const el = document.createElement("div");
    const workflow = injectContext(el, WorkflowServiceContext);

    effect(() => {
      const info = workflow.currentStepInfo.value;
      const loading = workflow.isLoading.value;
      const canRespond = workflow.canRespond.value;
      const value = (info?.actionConfig?.discountValue as string) ?? "10";

      untracked(() => {
        render(
          html`
            <h2>Take ${value}% off your next order?</h2>
            ${(info?.availableResponses ?? []).map(
              (r) => html`
                <button
                  ?disabled=${loading || !canRespond}
                  @click=${() => workflow.respond(r.type)}
                >
                  ${r.label}
                </button>
              `,
            )}
          `,
          el,
        );
      });
    });

    return el;
  },
});

Starting a flow

The host page typically starts a flow in response to a customer action — such as canceling a subscription or initiating a retention offer.
await workflow.startFlow("cancel_subscription", {
  subscriptionId: current.id,
  layout: "modal",
});
flowType is a stable client identifier. The backend selects the published workflow definition that matches the flow type for the trigger source.