# Client Onboarding Took 3 People and 6 Hours. Now It Takes One AI Agent and 90 Seconds.
> A 15-person consulting firm built a Struere agent that automates new client onboarding — sending welcome emails, creating Airtable workspaces, scheduling kickoff calls, and tracking document collection. 6 hours of admin per client dropped to 15 minutes of review.

Published: 2026-04-01
Tags: automation, agents, consulting, case-study, onboarding


# 3 People, 6 Hours, and Something Always Slips

Natasha is a partner at Meridian Advisory, a 15-person management consulting firm in Chicago that takes on 6-8 new engagements per month. The work is strategic — digital transformation, operational efficiency, M&A integration. The onboarding is not.

When a deal closes, the clock starts. The client expects a welcome email within 24 hours. They need a shared workspace with document templates, scope matrices, and a RACI chart. A kickoff call needs to land on everyone's calendar within the first week. The client needs to submit W-9s, NDAs, insurance certificates, and vendor registration forms before any billable work begins. And once the engagement starts, the client expects a weekly status email every Friday at 3 PM.

Three people touch this process. Natasha drafts the welcome email and personalizes it for the client's industry. The operations manager, Derek, creates the Airtable workspace from a template, shares it with the client team, and adds the project to the firm's master tracker. An associate, Jess, handles document collection — emailing the client a checklist, following up three days later, following up again a week later, then escalating to Natasha when something is still missing.

The whole sequence takes 4-6 hours spread across 2-3 days. Six engagements per month means 24-36 hours of pure admin. Billable rate at the firm averages $275/hour. That is $6,600-$9,900/month in opportunity cost spent on emails, spreadsheets, and calendar invites.

Last quarter, a new engagement with a $180K contract slipped through the cracks. Derek was on vacation. Jess assumed he had set up the workspace before leaving. He assumed she would cover it. The client received no welcome email, no workspace invite, and no kickoff calendar invite for 9 days. The partner at the client called Natasha directly: "Are we actually starting this engagement, or should I talk to McKinsey again?" They saved the relationship, but it took a dinner and a 10% scope discount to smooth it over. That dinner cost $400. The discount cost $18,000.

Natasha did not need another operations hire. She needed a system that fires the moment a deal closes and does not depend on anyone remembering.

# The Build: Event-Driven Onboarding Agent

The automation has five pieces:

1. **`entity-types/engagement.ts`** stores every client engagement with contact info, project details, document status, and onboarding progress
2. **`tools/index.ts`** has a custom tool that provisions an Airtable workspace from a template and shares it with the client
3. **`agents/onboarding-agent.ts`** orchestrates the full onboarding sequence: emails, workspace, calendar, document tracking
4. **`triggers/engagement-created.ts`** fires when a new engagement entity is created, dispatching the agent to run the onboarding playbook
5. **`triggers/weekly-status.ts`** fires when an engagement is updated to "active" status, scheduling weekly status digest emails

When a partner marks a deal as closed in the dashboard, they create an engagement entity. The trigger fires immediately. The agent sends the welcome email, provisions the Airtable workspace, schedules the kickoff call, and starts the document collection sequence. No handoff. No waiting for someone to remember.

## The Data Layer: What Gets Stored

Every closed deal creates an engagement record with 10 fields. The `onboardingStatus` enum tracks progress from initial outreach through fully onboarded.

```typescript
import { defineData } from "struere"

export default defineData({
  name: "Engagement",
  slug: "engagement",
  schema: {
    type: "object",
    properties: {
      clientName: {
        type: "string",
        description: "Client company name",
      },
      clientContactName: {
        type: "string",
        description: "Primary contact full name at the client",
      },
      clientContactEmail: {
        type: "string",
        format: "email",
        description: "Primary contact email address",
      },
      projectName: {
        type: "string",
        description: "Engagement project name (e.g., 'Q3 Digital Transformation')",
      },
      engagementType: {
        type: "string",
        enum: [
          "digital-transformation",
          "operational-efficiency",
          "ma-integration",
          "strategy-advisory",
          "process-optimization",
        ],
        description: "Type of consulting engagement",
      },
      leadPartner: {
        type: "string",
        description: "Meridian partner leading the engagement",
      },
      contractValue: {
        type: "number",
        description: "Total contract value in USD",
      },
      kickoffDate: {
        type: "string",
        description: "Target kickoff date in ISO format (YYYY-MM-DD)",
      },
      documentsReceived: {
        type: "array",
        items: { type: "string" },
        description: "List of received document types (e.g., 'nda', 'w9', 'insurance-cert')",
      },
      onboardingStatus: {
        type: "string",
        enum: [
          "pending-welcome",
          "welcome-sent",
          "workspace-created",
          "kickoff-scheduled",
          "collecting-documents",
          "onboarded",
        ],
        description: "Current step in the onboarding process",
      },
    },
    required: [
      "clientName",
      "clientContactName",
      "clientContactEmail",
      "projectName",
      "engagementType",
      "leadPartner",
      "contractValue",
      "onboardingStatus",
    ],
  },
  searchFields: ["clientName", "clientContactName", "projectName"],
  displayConfig: {
    titleField: "clientName",
    subtitleField: "projectName",
    descriptionField: "onboardingStatus",
  },
})
```

The `engagementType` enum maps to Meridian's five practice areas. Each type drives different Airtable templates and document requirements. The `documentsReceived` array starts empty and grows as the client submits files — the agent checks this list before sending follow-up reminders. The `onboardingStatus` field is the backbone: triggers and agent logic both key off of it to determine what happens next.

## The Custom Tool: Airtable Workspace Provisioning

One custom tool creates a new Airtable base from a template and shares it with the client contact. The agent calls this after sending the welcome email.

```typescript
import { defineTools } from "struere"

export default defineTools([
  {
    name: "provision_client_workspace",
    description:
      "Create a new Airtable workspace for a client engagement from a template and share it with the client contact",
    parameters: {
      type: "object",
      properties: {
        clientName: {
          type: "string",
          description: "Client company name for the workspace title",
        },
        engagementType: {
          type: "string",
          description: "Engagement type to select the correct template",
        },
        clientContactEmail: {
          type: "string",
          description: "Email address to share the workspace with",
        },
      },
      required: ["clientName", "engagementType", "clientContactEmail"],
    },
    handler: async (args, context, struere, fetch) => {
      const templateMap: Record<string, string> = {
        "digital-transformation": "tblDT_TEMPLATE_ID",
        "operational-efficiency": "tblOE_TEMPLATE_ID",
        "ma-integration": "tblMA_TEMPLATE_ID",
        "strategy-advisory": "tblSA_TEMPLATE_ID",
        "process-optimization": "tblPO_TEMPLATE_ID",
      }

      const templateId = templateMap[args.engagementType as string]
      if (!templateId) {
        return { error: `No template for engagement type: ${args.engagementType}` }
      }

      const bases = await struere.airtable.listBases()
      const existingBase = bases.bases?.find(
        (b: { name: string }) =>
          b.name === `${args.clientName} — Engagement`
      )

      if (existingBase) {
        return {
          baseId: existingBase.id,
          name: existingBase.name,
          alreadyExists: true,
        }
      }

      const records = await struere.airtable.createRecords({
        baseId: templateId,
        tableIdOrName: "Project Setup",
        records: [
          {
            fields: {
              "Client Name": args.clientName,
              "Contact Email": args.clientContactEmail,
              Status: "Active",
            },
          },
        ],
      })

      return {
        templateUsed: args.engagementType,
        clientName: args.clientName,
        sharedWith: args.clientContactEmail,
        setupRecord: records,
      }
    },
  },
])
```

The tool checks for an existing workspace before creating a new one — a safeguard against duplicate provisioning if the trigger fires twice. The template map routes each engagement type to a pre-built Airtable base with the correct deliverable trackers, RACI charts, and milestone tables. The agent does not need to know what is in each template. It creates the workspace and the client gets a ready-to-use project hub.

## The Agent: Full Onboarding Orchestration

The agent runs the entire onboarding playbook in a single execution. The system prompt encodes the step-by-step sequence, email templates, document requirements, and escalation rules.

```typescript
import { defineAgent } from "struere"

export default defineAgent({
  name: "Meridian Onboarding Agent",
  slug: "onboarding-agent",
  version: "1.0.0",
  model: {
    model: "anthropic/claude-sonnet-4-6",
    temperature: 0.3,
    maxTokens: 4096,
  },
  tools: [
    "entity.query",
    "entity.update",
    "email.send",
    "calendar.create",
    "provision_client_workspace",
  ],
  systemPrompt: `You are the onboarding coordinator for Meridian Advisory, a management consulting firm.
Current time: {{currentTime}}

## Active Engagements
{{entity.query({"type": "engagement", "filters": {"onboardingStatus": {"_op_in": ["pending-welcome", "welcome-sent", "workspace-created", "kickoff-scheduled", "collecting-documents"]}}, "limit": 20})}}

## Onboarding Playbook

When triggered by a new engagement, execute these steps in order:

### Step 1 — Welcome Email
Send a personalized welcome email to the client contact via email.send:
- Subject: "Welcome to Meridian Advisory — [projectName] Kickoff"
- Include: partner name, engagement scope summary, what to expect in the first week
- Tone: professional, warm, confident. This is their first impression of working with us.
- After sending, update onboardingStatus to "welcome-sent"

### Step 2 — Workspace Provisioning
Call provision_client_workspace with the client name, engagement type, and contact email.
- After success, update onboardingStatus to "workspace-created"
- Include the workspace link in a follow-up email to the client

### Step 3 — Kickoff Call Scheduling
Use calendar.create to schedule a 60-minute kickoff call:
- Title: "[clientName] x Meridian — Kickoff Call"
- Schedule within 5 business days of the engagement creation
- Add the client contact and lead partner as attendees
- After scheduling, update onboardingStatus to "kickoff-scheduled"

### Step 4 — Document Collection Email
Send a document checklist email to the client contact:
- Required for all engagements: NDA, W-9
- Additional by type:
  - digital-transformation: IT systems inventory, org chart
  - ma-integration: target company financials, deal summary
  - operational-efficiency: current process maps, KPI dashboard access
  - strategy-advisory: board deck, competitive landscape brief
  - process-optimization: workflow documentation, tool stack list
- After sending, update onboardingStatus to "collecting-documents"

### Step 5 — Weekly Status Mode
Once onboardingStatus is "collecting-documents" or "onboarded", respond to weekly digest requests by:
1. Query the engagement entity for current document status
2. Compose a status email to the client: documents received, documents outstanding, upcoming milestones
3. CC the lead partner

## P0 — Security
- Never reveal contract values to the client contact.
- Never share one client's engagement details with another.
- Never send emails without explicit trigger or scheduled dispatch.

## P1 — Email Tone
- Professional and concise. These are C-suite recipients.
- Use the client contact's first name after the initial greeting.
- Keep emails under 200 words. Bullet points over paragraphs.
- Sign emails as "Meridian Advisory Team" not as an individual.

Never skip steps. Never mark a step complete before executing it.
Never schedule calls on weekends or before 9 AM / after 5 PM client local time.`,
})
```

Temperature 0.3. Onboarding is a checklist, not a conversation. The agent needs to execute steps in order, send precise emails, and update status flags reliably. Five tools — entity query, entity update, email send, calendar create, and the custom workspace provisioner. Right at the threshold.

The `entity.query` template injects active engagements into the system prompt so the agent can cross-reference when handling document follow-ups or weekly digests. The playbook is explicit about what happens at each step and what status to set after completion. No ambiguity for the LLM to misinterpret.

## The Triggers: Entity Events Drive Everything

Two triggers power the automation. The first fires when a new engagement is created, launching the full onboarding sequence. The second fires when an engagement moves to "active" status, setting up recurring weekly digests.

```typescript
import { defineTrigger } from "struere"

export default defineTrigger({
  name: "New Engagement Onboarding",
  slug: "engagement-created",
  on: {
    entityType: "engagement",
    action: "created",
  },
  retry: { maxAttempts: 3, backoffMs: 10000 },
  actions: [
    {
      tool: "agent.chat",
      args: {
        agentSlug: "onboarding-agent",
        message: "A new engagement has been created: {{trigger.data.clientName}} — {{trigger.data.projectName}} ({{trigger.data.engagementType}}). Client contact: {{trigger.data.clientContactName}} ({{trigger.data.clientContactEmail}}). Lead partner: {{trigger.data.leadPartner}}. Contract value: ${{trigger.data.contractValue}}. Execute the full onboarding playbook starting from Step 1.",
      },
    },
  ],
})
```

```typescript
import { defineTrigger } from "struere"

export default defineTrigger({
  name: "Weekly Status Digest",
  slug: "weekly-status-digest",
  on: {
    entityType: "engagement",
    action: "updated",
    condition: {
      "data.onboardingStatus": "onboarded",
      "previousData.onboardingStatus": "collecting-documents",
    },
  },
  schedule: {
    delay: 604800000,
    cancelPrevious: true,
  },
  retry: { maxAttempts: 2, backoffMs: 30000 },
  actions: [
    {
      tool: "agent.chat",
      args: {
        agentSlug: "onboarding-agent",
        message: "Send the weekly status digest for engagement: {{trigger.data.clientName}} — {{trigger.data.projectName}}. Client contact: {{trigger.data.clientContactEmail}}. Lead partner: {{trigger.data.leadPartner}}. Check current document status and upcoming milestones.",
      },
    },
  ],
})
```

The first trigger has no conditions — every new engagement gets the full onboarding treatment. Retry is set to 3 attempts with 10-second backoff because the sequence involves three external services (Resend, Airtable, Google Calendar) and any of them can hiccup.

The second trigger uses a state transition condition: it only fires when `onboardingStatus` moves from `collecting-documents` to `onboarded`. The `schedule.delay` of 604,800,000 milliseconds (7 days) means the weekly digest fires one week after the engagement is fully onboarded, and `cancelPrevious` ensures only one pending digest exists per engagement at a time.

# Debugging: Three Things That Broke

**The kickoff call was scheduled at 7 AM in the client's timezone.** The agent used `calendar.create` with a time based on Meridian's Chicago timezone (Central), but the client was in Seattle (Pacific). A 9 AM Central call showed up as 7 AM Pacific on the client's calendar. The system prompt said "never schedule before 9 AM client local time" but the agent had no way to know the client's timezone.

Fix: we added a `timezone` field to the engagement entity schema defaulting to `America/Chicago` and updated the system prompt to include timezone-aware scheduling instructions. The partner now sets the client's timezone when creating the engagement, and the agent passes it to `calendar.create`.

```bash
struere triggers logs engagement-created
```

The execution log showed `calendar.create` called with `startTime: "2026-03-15T09:00:00-06:00"` — correct for Chicago, wrong for a Seattle client. After adding the timezone field, the agent generates `startTime: "2026-03-15T09:00:00-08:00"` for Pacific timezone clients.

**The Airtable workspace tool returned an error because the template base ID was wrong.** During initial setup, the template IDs in the tool handler were placeholders. The tool returned `{ error: "No template for engagement type: digital-transformation" }` even though the type was valid — the template map had the right keys but the base IDs pointed to nonexistent Airtable bases. The trigger completed with a partial onboarding: welcome email sent, workspace creation failed silently, kickoff call scheduled.

Fix: we tested the tool in isolation before running the full trigger. The `run-tool` command showed the exact error without needing to fire the entire onboarding sequence.

```bash
struere run-tool provision_client_workspace --args '{"clientName": "Test Corp", "engagementType": "digital-transformation", "clientContactEmail": "test@example.com"}'
```

The output returned `{ error: "AIRTABLE_API_ERROR: Base not found" }`. After replacing placeholder IDs with real Airtable base IDs, the tool returned a successful provisioning response.

**The document collection email listed requirements for the wrong engagement type.** An M&A integration engagement received the document checklist for operational efficiency — requesting "current process maps" and "KPI dashboard access" instead of "target company financials" and "deal summary." The agent had the engagement type in the trigger context but the system prompt's document requirements section used conditional logic that the LLM misapplied.

Fix: we restructured the document requirements section in the system prompt to use explicit mapping instead of bullet-point conditionals. Instead of "Additional by type:" with a nested list, we switched to a flat lookup format: "If engagementType is 'ma-integration', required documents are: NDA, W-9, target company financials, deal summary." Explicit beats implicit when an LLM is following instructions.

```bash
struere logs view --last 3
```

The conversation log showed the agent's reasoning: it identified the engagement type as `ma-integration` but then listed documents from the `operational-efficiency` section, which appeared earlier in the prompt. Moving to explicit per-type blocks eliminated the cross-contamination.

# Setup: From Zero to Running

Install the CLI and authenticate:

```bash
bun install -g struere
struere login
struere init
```

Scaffold the resources:

```bash
struere add data-type engagement
struere add agent onboarding-agent
struere add trigger engagement-created
struere add trigger weekly-status-digest
```

Edit `entity-types/engagement.ts` with the schema shown above. Customize the `engagementType` enum to match your firm's practice areas. Add or remove document types in the `documentsReceived` array description to match your compliance requirements.

Edit `tools/index.ts` with the Airtable workspace provisioner. Replace the placeholder template IDs with your actual Airtable base IDs. Create one template base per engagement type with the deliverable trackers, RACI charts, and milestone tables your firm uses.

Edit `agents/onboarding-agent.ts` with your firm's details: partner names, email templates, document requirements per engagement type, and scheduling rules. Customize the P1 tone section for your firm's voice.

Edit `triggers/engagement-created.ts` and `triggers/weekly-status-digest.ts` with the configurations shown above.

Configure Resend under Integrations > Email with your API key and sender address. Use a professional domain like `onboarding@meridianadvisory.com`.

Add your Airtable personal access token under Integrations > Airtable. Ensure the token has access to your template bases.

Connect Google Calendar under Integrations > Google Calendar. Each partner needs a connected calendar so `calendar.create` can schedule kickoff calls on the correct calendar.

Sync and start watching:

```bash
struere dev
```

Test the full onboarding by creating a test engagement:

```bash
struere run-tool entity.create --args '{"type": "engagement", "data": {"clientName": "Acme Corp", "clientContactName": "Sarah Chen", "clientContactEmail": "sarah@acme.com", "projectName": "Q2 Process Optimization", "engagementType": "process-optimization", "leadPartner": "Natasha", "contractValue": 95000, "onboardingStatus": "pending-welcome"}}'
```

Watch the trigger fire and the onboarding sequence execute:

```bash
struere triggers logs engagement-created
```

Verify each step completed:

```bash
struere data list engagement
```

The engagement's `onboardingStatus` should progress from `pending-welcome` through `welcome-sent`, `workspace-created`, `kickoff-scheduled`, and finally `collecting-documents` — all within a single trigger execution.

# What Changed

| Metric | Before | After |
|--------|--------|-------|
| People involved in onboarding | 3 (partner + ops + associate) | 1 (partner reviews) |
| Time per client onboarding | 4-6 hours across 2-3 days | 15 minutes of partner review |
| Time to first client contact | 12-48 hours | Under 2 minutes |
| Missed onboarding steps/quarter | 4-6 | 0 |
| Document follow-up emails (manual) | 3-4 per client | 0 (automated) |
| Monthly admin hours on onboarding | 24-36 hours | 3-4 hours |
| Opportunity cost saved/month | $6,600-$9,900 | Reallocated to billable work |

Derek still manages the master project tracker. Jess still handles complex client requests that require judgment. But the mechanical sequence — email, workspace, calendar, documents, follow-ups — runs itself. When Natasha creates an engagement entity in the dashboard, she walks away. Ninety seconds later, the client has a welcome email in their inbox, a shared Airtable workspace, a kickoff call on their calendar, and a document checklist with deadlines.

The $180K engagement that nearly walked last quarter would not have slipped. The moment the deal closed and the entity was created, the trigger would have fired. The client would have received a welcome email before Natasha finished her coffee. No one would need to remember. No one would need to hand off. No vacation would create a gap.

Natasha still reviews each onboarding for the personal touch — a note about something discussed during the sales process, a mention of the client's recent funding round. That takes 15 minutes. The other 5 hours and 45 minutes of admin are gone. At $275/hour, that is $1,581 per engagement redirected from admin work to billable consulting. Six engagements per month. $9,487 recaptured. Not as a line item on a spreadsheet, but as hours spent doing the work clients actually pay for.
