> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runflow.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer Onboarding Assistant

> Build an interactive onboarding agent that guides users step by step and tracks progress

An interactive onboarding agent that guides new users through setup, tracks their progress, and adapts to their pace. This example shows long-running conversations with progress tracking, knowledge base search, and dynamic instructions based on user state.

## Project Structure

```
onboarding-agent/
├── main.ts
├── agent.ts
├── tools/
│   ├── index.ts
│   ├── mark-step-complete.ts
│   └── get-progress.ts
├── prompts/
│   └── index.ts
├── config/
│   └── steps.ts
├── .runflow/
│   └── rf.json
├── package.json
└── tsconfig.json
```

## Step 1: Define Onboarding Steps

Keep step definitions in a config file so they're easy to update:

```typescript config/steps.ts theme={null}
export const ONBOARDING_STEPS = [
  { id: 'profile', name: 'Complete your profile', description: 'Set up name, photo, and preferences' },
  { id: 'workspace', name: 'Create a workspace', description: 'Set up your first workspace' },
  { id: 'invite', name: 'Invite team members', description: 'Add at least one teammate' },
  { id: 'first-agent', name: 'Create your first agent', description: 'Build and test a simple agent' },
  { id: 'deploy', name: 'Deploy to production', description: 'Deploy your agent live' },
] as const;

export type StepId = (typeof ONBOARDING_STEPS)[number]['id'];
```

## Step 2: Progress Tools

Tools for tracking and querying onboarding progress:

```typescript tools/get-progress.ts theme={null}
import { createTool } from '@runflow-ai/sdk';
import { z } from 'zod';
import { ONBOARDING_STEPS } from '../config/steps';

export const getProgressTool = createTool({
  id: 'get-progress',
  description: 'Get the current onboarding progress for a user',
  inputSchema: z.object({}),
  execute: async (params, toolContext) => {
    // Fetch from your database
    const completed = await fetchCompletedSteps(toolContext.userId);

    const steps = ONBOARDING_STEPS.map((step) => ({
      ...step,
      completed: completed.includes(step.id),
    }));

    const completedCount = steps.filter((s) => s.completed).length;
    const nextStep = steps.find((s) => !s.completed);

    return {
      steps,
      completedCount,
      totalSteps: steps.length,
      progress: `${completedCount}/${steps.length}`,
      nextStep: nextStep || null,
      isComplete: completedCount === steps.length,
    };
  },
});
```

```typescript tools/mark-step-complete.ts theme={null}
import { createTool } from '@runflow-ai/sdk';
import { track } from '@runflow-ai/sdk/observability';
import { z } from 'zod';
import { ONBOARDING_STEPS } from '../config/steps';

const validStepIds = ONBOARDING_STEPS.map((s) => s.id) as [string, ...string[]];

export const markStepCompleteTool = createTool({
  id: 'mark-step-complete',
  description: 'Mark an onboarding step as completed',
  inputSchema: z.object({
    stepId: z.enum(validStepIds).describe('The step to mark as complete'),
    notes: z.string().optional().describe('Optional notes about completion'),
  }),
  execute: async (params, toolContext) => {
    try {
      // Check if already completed
      const completed = await fetchCompletedSteps(toolContext.userId);
      if (completed.includes(params.stepId)) {
        return { success: true, alreadyCompleted: true, stepId: params.stepId };
      }

      // Mark as completed
      await saveStepCompletion(toolContext.userId, params.stepId, params.notes);

      const newCompleted = completed.length + 1;
      const total = ONBOARDING_STEPS.length;

      track('onboarding_step_completed', {
        stepId: params.stepId,
        progress: `${newCompleted}/${total}`,
      });

      // Check if onboarding is now complete
      if (newCompleted === total) {
        track('onboarding_completed', {
          totalSteps: total,
        });
      }

      return {
        success: true,
        stepId: params.stepId,
        progress: `${newCompleted}/${total}`,
        isOnboardingComplete: newCompleted === total,
      };
    } catch (error) {
      return { success: false, error: 'Failed to save progress' };
    }
  },
});
```

```typescript tools/index.ts theme={null}
export { getProgressTool } from './get-progress';
export { markStepCompleteTool } from './mark-step-complete';
```

## Step 3: Prompt

```typescript prompts/index.ts theme={null}
import { ONBOARDING_STEPS } from '../config/steps';

const stepList = ONBOARDING_STEPS.map((s, i) => `${i + 1}. **${s.name}**: ${s.description}`).join('\n');

export const onboardingPrompt = `You are a friendly onboarding assistant that guides new users through setup.

## Onboarding Steps
${stepList}

## Behavior
- Start by checking the user's current progress with get-progress
- Guide them through the next incomplete step
- Celebrate when they complete a step — use encouragement
- If they're stuck, offer tips and link to relevant docs
- If they ask about something unrelated, gently steer back to onboarding
- Adapt your pace — if they seem experienced, be brief; if they seem new, explain more

## Tools
- Use **get-progress** at the start of each conversation to know where they are
- Use **mark-step-complete** when the user confirms they've finished a step
- Never mark a step as complete without the user confirming it

## Response Style
- Be warm and encouraging but not over-the-top
- Use short paragraphs
- Use numbered steps when explaining how to do something
- End messages with a clear next action`;
```

## Step 4: Agent Definition

```typescript agent.ts theme={null}
import { Agent, openai } from '@runflow-ai/sdk';
import { onboardingPrompt } from './prompts';
import { getProgressTool, markStepCompleteTool } from './tools';

export const onboardingAgent = new Agent({
  name: 'Onboarding Assistant',
  instructions: onboardingPrompt,
  model: openai('gpt-4o'),

  memory: {
    maxTurns: 50,
    summarizeAfter: 30,
    summarizePrompt: 'Summarize: completed onboarding steps, current step, user questions, and blockers',
  },

  rag: {
    vectorStore: 'onboarding-docs',
    k: 3,
    threshold: 0.7,
    searchPrompt: `Search when the user asks how to do something specific, like:
- How to create a workspace
- How to invite team members
- How to deploy an agent`,
  },

  tools: {
    getProgress: getProgressTool,
    markStepComplete: markStepCompleteTool,
  },

  observability: 'full',
});
```

## Step 5: Main Entry Point

```typescript main.ts theme={null}
import { identify, track } from '@runflow-ai/sdk/observability';
import { onboardingAgent } from './agent';

export async function main(input: any) {
  if (!input?.message) {
    return { error: 'message is required' };
  }

  const userId = input.email || input.userId;
  if (!userId) {
    return { error: 'email or userId is required for onboarding' };
  }

  identify(userId);

  try {
    const result = await onboardingAgent.process({
      message: input.message,
      sessionId: `onboarding_${userId}`,
    });

    track('onboarding_interaction', {
      channel: input.channel || 'api',
    });

    return { message: result.message };
  } catch (error) {
    console.error('[onboarding] Error:', error);
    return { error: 'Something went wrong. Please try again.' };
  }
}
```

## Key Patterns

### Long-Running Conversations

Onboarding happens over days or weeks. Use high `maxTurns` and `summarizeAfter` to preserve context without losing important progress information:

```typescript theme={null}
memory: {
  maxTurns: 50,
  summarizeAfter: 30,
  summarizePrompt: 'Summarize: completed steps, current step, user questions, blockers',
}
```

### Progress-Aware Agent

The agent calls `get-progress` at the start of each conversation to know exactly where the user is. This means it can pick up right where they left off — even days later.

### Step Validation with Enums

Using TypeScript enums for step IDs ensures the LLM can only mark valid steps as complete:

```typescript theme={null}
const validStepIds = ONBOARDING_STEPS.map((s) => s.id) as [string, ...string[]];

inputSchema: z.object({
  stepId: z.enum(validStepIds),  // LLM can only choose from valid steps
})
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Memory" icon="brain" href="/core-concepts/memory">
    Long conversation management
  </Card>

  <Card title="Knowledge (RAG)" icon="database" href="/core-concepts/knowledge-rag">
    Power the docs search
  </Card>

  <Card title="Customer Support" icon="headset" href="/use-cases/customer-support-rag">
    Support agent example
  </Card>

  <Card title="Best Practices" icon="lightbulb" href="/best-practices">
    Tips for effective agents
  </Card>
</CardGroup>
