Skip to main content

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.

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 (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,
    };
  },
});
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 (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' };
    }
  },
});
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