Skip to main content
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:
config/steps.ts
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:
tools/get-progress.ts
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 ({ runflow }) => {
    // Fetch from your database
    const completed = await fetchCompletedSteps(runflow.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,
    };
  },
});
tools/mark-step-complete.ts
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 ({ context, runflow }) => {
    try {
      // Check if already completed
      const completed = await fetchCompletedSteps(runflow.userId);
      if (completed.includes(context.stepId)) {
        return { success: true, alreadyCompleted: true, stepId: context.stepId };
      }

      // Mark as completed
      await saveStepCompletion(runflow.userId, context.stepId, context.notes);

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

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

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

      return {
        success: true,
        stepId: context.stepId,
        progress: `${newCompleted}/${total}`,
        isOnboardingComplete: newCompleted === total,
      };
    } catch (error) {
      return { success: false, error: 'Failed to save progress' };
    }
  },
});
tools/index.ts
export { getProgressTool } from './get-progress';
export { markStepCompleteTool } from './mark-step-complete';

Step 3: Prompt

prompts/index.ts
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

agent.ts
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

main.ts
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:
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:
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

Memory

Long conversation management

Knowledge (RAG)

Power the docs search

Customer Support

Support agent example

Best Practices

Tips for effective agents