← All examples
CompositionEventsLocal

Modular customer onboarding

Run several readable workflows as one local system. Call a workflow for its result, start independent work, and publish an event to a subscriber.

This example is runnable without a cloud account, email provider, or external credentials. It stores a workspace record in WOML state, writes a welcome message to a local outbox object, and records the customer event. The welcome workflow prepares a message for application delivery; it does not send an email.

Files and responsibilities

FileResponsibilityRelationship
customer-onboarding.womlCoordinate onboardingManual entry point
provision-workspace.womlCreate or reuse a workspace recordCaller waits for its result
send-welcome.womlPrepare an outbox messageStarts independently
record-customer-event.womlStore the accepted customer eventSubscribes to customer.created

Save all four files in one workflows folder. Their workflow IDs are distinct from the standalone Customer Onboarding example.

customer-onboarding.woml

WOML
<woml>
  <workflow id="modular-onboarding" name="Modular customer onboarding">
    <triggers><manual id="start" /></triggers>
    <steps>
      <step id="customer" name="Prepare the customer">
        <script>
          return { id: "customer-001", email: "ada@onboarding.test" };
        </script>
      </step>
      <step id="workspace" name="Wait for a workspace">
        <script>
          return services.workflows.call(
            "provision-workspace", context.steps.customer,
            { name: "provision-customer-workspace" }
          );
        </script>
      </step>
      <step id="welcome" name="Start the welcome workflow">
        <script>
          return services.workflows.start(
            "send-welcome",
            { ...context.steps.customer, workspaceId: context.steps.workspace.id },
            { name: "start-customer-welcome" }
          );
        </script>
      </step>
      <step id="publish" name="Publish customer.created">
        <script>
          const event = await services.events.emit(
            "customer.created",
            { ...context.steps.customer, workspaceId: context.steps.workspace.id },
            { name: "publish-created-customer" }
          );
          return {
            customerId: context.steps.customer.id,
            workspaceId: context.steps.workspace.id,
            welcomeRunId: context.steps.welcome.runId,
            subscribers: event.acceptedCount
          };
        </script>
      </step>
    </steps>
  </workflow>
</woml>

provision-workspace.woml

WOML
<woml>
  <workflow id="provision-workspace" name="Provision a workspace record">
    <steps>
      <step id="workspace" name="Create or reuse the workspace">
        <script>
          const key = "workspace/" + context.payload.id;
          await services.state.setIfAbsent(
            key,
            { id: "workspace-" + context.payload.id, customerId: context.payload.id },
            { name: "create-workspace-record" }
          );
          const stored = await services.state.get(key);
          return stored.value;
        </script>
      </step>
    </steps>
  </workflow>
</woml>

send-welcome.woml

WOML
<woml>
  <workflow id="send-welcome" name="Prepare the welcome message">
    <steps>
      <step id="outbox" name="Save a message for application delivery">
        <script>
          const message = await services.storage.put({
            key: "welcome/" + context.payload.id + ".json",
            value: {
              recipient: context.payload.email,
              workspaceId: context.payload.workspaceId,
              text: "Your workspace is ready."
            },
            overwrite: true
          });
          return { status: "outbox-ready", key: message.key };
        </script>
      </step>
    </steps>
  </workflow>
</woml>

record-customer-event.woml

WOML
<woml>
  <workflow id="record-customer-event" name="Record the customer event">
    <triggers><event id="created" name="customer.created" /></triggers>
    <steps>
      <step id="record" name="Remember the created customer">
        <script>
          await services.state.set(
            "customer/" + context.payload.id,
            context.payload,
            { name: "record-created-customer" }
          );
          return { status: "recorded", customerId: context.payload.id };
        </script>
      </step>
    </steps>
  </workflow>
</woml>

Validate and run the complete system

Terminal
woml check workflows/
woml run workflows/

Press Enter to start modular-onboarding. Its manual trigger creates one sample customer. The workspace workflow returns the created or existing record before the parent continues. The welcome workflow is durably started, then the parent publishes customer.created.

The parent's result includes the customer ID, workspace ID, independent welcome run ID, and accepted subscriber count. The started workflow and event subscriber have their own outcomes; parent completion does not mean either has finished successfully.

Use the welcome run ID with woml get, or inspect the active runtime:

Terminal
woml inspect

Reuse and boundaries

Repeated starts reuse the workflow-owned workspace record. The welcome object uses a stable customer key with explicit overwrite. The event recorder updates the same customer state key.

Keep related workflows active together and use the same durable state boundary. Directory discovery loads direct .woml files, not nested folders. Starting a child durably admits the work; it does not guarantee its later success. Cancelling a parent does not automatically cancel independent children.

A synchronous call must not target a workflow containing human approval. For long-lived review, start independent work or use events instead.

To adapt this to an application, use an authenticated webhook in the parent, import your account-provisioning API in the workspace workflow, and replace the local welcome outbox with your authorized delivery service. Those integrations require your own credentials and failure-handling policy.