← All examples
BackendSQLiteState

Customer Onboarding

Create accounts, provision workspaces, and coordinate multi-step onboarding.

This runnable local example creates account and workspace records in an application-owned SQLite database, then saves the completed onboarding outcome in WOML durable state. It uses the same workflow structure you can connect to your own identity and workspace services.

How the workflow works

  1. Create an account, or find the existing account by email.
  2. Provision a workspace record for that account, or reuse its existing workspace.
  3. Record the account, workspace, and ready status in workflow-owned state.

The unique email and account constraints make another manual run reuse the same account and workspace.

Create the workflow

Save the following as customer-onboarding.woml in a writable project folder:

WOML
<woml>
  <workflow
    id="customer-onboarding"
    name="Customer onboarding"
    description="Create a local account, provision its workspace, and record onboarding."
  >
    <triggers>
      <manual id="start" />
    </triggers>

    <steps>
      <step id="createAccount" name="Create the customer account">
        <script>
          const db = services.db({
            driver: "sqlite",
            connection: "./onboarding.sqlite"
          });
          await db.execute({
            text: "CREATE TABLE IF NOT EXISTS accounts (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL)"
          }, { name: "ensure-accounts" });

          const email = context.payload.email ?? "ada@onboarding.test";
          const name = context.payload.name ?? "Ada Lovelace";
          await db.execute({
            text: "INSERT INTO accounts (id, email, name) VALUES (?, ?, ?) ON CONFLICT(email) DO NOTHING",
            values: [crypto.randomUUID(), email, name]
          }, { name: "create-account" });

          const result = await db.query({
            text: "SELECT id, email, name FROM accounts WHERE email = ?",
            values: [email]
          }, { name: "load-account" });
          return result.rows[0];
        </script>
      </step>

      <step id="provisionWorkspace" name="Provision the workspace">
        <script>
          const db = services.db({
            driver: "sqlite",
            connection: "./onboarding.sqlite"
          });
          await db.execute({
            text: "CREATE TABLE IF NOT EXISTS workspaces (id TEXT PRIMARY KEY, account_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL)"
          }, { name: "ensure-workspaces" });

          const account = context.steps.createAccount;
          await db.execute({
            text: "INSERT INTO workspaces (id, account_id, name) VALUES (?, ?, ?) ON CONFLICT(account_id) DO NOTHING",
            values: [crypto.randomUUID(), account.id, account.name + "'s workspace"]
          }, { name: "create-workspace" });

          const result = await db.query({
            text: "SELECT id, account_id, name FROM workspaces WHERE account_id = ?",
            values: [account.id]
          }, { name: "load-workspace" });
          return result.rows[0];
        </script>
      </step>

      <step id="complete" name="Record completed onboarding">
        <script>
          const outcome = {
            accountId: context.steps.createAccount.id,
            workspaceId: context.steps.provisionWorkspace.id,
            email: context.steps.createAccount.email,
            status: "ready"
          };
          await services.state.set(
            "onboarding/" + outcome.accountId,
            outcome,
            { name: "record-onboarding" }
          );
          return outcome;
        </script>
      </step>
    </steps>
  </workflow>
</woml>

Validate and run

Terminal
woml check customer-onboarding.woml
woml run customer-onboarding.woml

Press Enter to start onboarding. The manual example uses Ada Lovelace and ada@onboarding.test; a webhook or another workflow can supply name and email through the trigger payload.

A successful run returns the created account ID, workspace ID, email, and status: "ready". The application records live in onboarding.sqlite, separate from WOML's runtime database. Keep both the application database and .woml/ runtime data out of source control.

Connect the process to your application

Replace the manual trigger with an authenticated webhook when your application should start onboarding. Move identity-provider calls or workspace provisioning into named exports in a local JavaScript or TypeScript module, then import that module into the workflow. Keep credentials in WOML secrets and pass only the values each operation needs.

Account creation and workspace provisioning are separate effects. Define compensation when your application requires an earlier successful operation to be corrected after a later failure.