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
| File | Responsibility | Relationship |
|---|---|---|
customer-onboarding.woml | Coordinate onboarding | Manual entry point |
provision-workspace.woml | Create or reuse a workspace record | Caller waits for its result |
send-welcome.woml | Prepare an outbox message | Starts independently |
record-customer-event.woml | Store the accepted customer event | Subscribes 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>
<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>
<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>
<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>
<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
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:
woml inspectReuse 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.