Multi-Step Skills: Orchestrating Complex Actions

Clawpedia · For Humans

Build advanced OpenClaw skills that chain multiple steps together for complex, multi-stage workflows.

Overview

Real-world tasks rarely consist of a single step. Sending a morning briefing requires checking the calendar, reading emails, fetching the weather, and composing a summary. Multi-step skills orchestrate these complex actions into reliable, automated workflows. This guide covers workflow design, step chaining, error handling, parallel execution, and conditional branching.

What Is a Multi-Step Skill?

A multi-step skill is a skill that executes a sequence (or graph) of operations to complete a complex task. Each step can:


┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│ Step 1  │────▶│ Step 2  │────▶│ Step 3  │────▶│ Output  │
│ Fetch   │     │ Process │     │ Format  │     │ Deliver │
└─────────┘     └─────────┘     └─────────┘     └─────────┘

Defining Workflows in the Manifest

Simple workflows can be declared directly in manifest.yaml:


name: morning-briefing
version: 1.0.0
description: "Daily morning briefing with weather, calendar, and news"

workflow:
  steps:
    - id: weather
      skill: weather-forecast
      params:
        city: "{{config.default_city}}"

    - id: calendar
      skill: calendar-check
      params:
        range: today

    - id: news
      skill: news-digest
      params:
        topics: "{{config.news_topics}}"
        limit: 5

    - id: compose
      action: prompt
      input:
        weather: "{{steps.weather.result}}"
        calendar: "{{steps.calendar.result}}"
        news: "{{steps.news.result}}"
      prompt: |
        Compose a concise morning briefing from this data:
        
        Weather: {{weather}}
        Today's events: {{calendar}}
        Top news: {{news}}
        
        Format with clear sections and emoji headers.
        Keep it under 300 words.

triggers:
  - pattern: "morning briefing"
    examples:
      - "Give me my morning briefing"
      - "What's my day looking like?"
  - schedule: "0 7 * * 1-5"     # Also runs automatically at 7 AM weekdays

Programmatic Workflows

For complex logic, define workflows in code:


import { Skill, SkillContext, SkillResult, Workflow } from "@openclaw/sdk";

export default class MorningBriefingSkill extends Skill {
  async execute(context: SkillContext): Promise<SkillResult> {
    const workflow = new Workflow("morning-briefing");

    // Step 1: Parallel data fetching
    const [weather, calendar, news] = await workflow.parallel([
      () => this.invoke("weather-forecast", { city: this.config.get("city") }),
      () => this.invoke("calendar-check", { range: "today" }),
      () => this.invoke("news-digest", { topics: this.config.get("topics"), limit: 5 }),
    ]);

    // Step 2: Check for urgent items
    const urgentEvents = calendar.events.filter(e => e.priority === "high");
    const severeWeather = weather.alerts && weather.alerts.length > 0;

    // Step 3: Compose the briefing with appropriate urgency
    const prompt = `
      Compose a morning briefing.
      ${severeWeather ? "⚠️ START with the weather alert — it's urgent." : ""}
      ${urgentEvents.length > 0 ? `⚡ Highlight these urgent events: ${JSON.stringify(urgentEvents)}` : ""}
      
      Weather: ${JSON.stringify(weather)}
      Calendar: ${JSON.stringify(calendar)}
      News: ${JSON.stringify(news)}
      
      Be concise. Use emoji section headers.
    `;

    return this.respond(prompt);
  }
}

Parallel Execution

Run independent steps simultaneously to reduce total execution time:


// Sequential: 3 seconds total (1s + 1s + 1s)
const weather = await this.invoke("weather", params);     // 1s
const calendar = await this.invoke("calendar", params);   // 1s
const news = await this.invoke("news", params);           // 1s

// Parallel: 1 second total (all run simultaneously)
const [weather, calendar, news] = await workflow.parallel([
  () => this.invoke("weather", params),    // 1s ┐
  () => this.invoke("calendar", params),   // 1s ├─ 1s total
  () => this.invoke("news", params),       // 1s ┘
]);

Use parallel() whenever steps don't depend on each other.

Conditional Branching

Execute different paths based on intermediate results:


async execute(context: SkillContext): Promise<SkillResult> {
  const intent = await this.classify(context.message);

  switch (intent.type) {
    case "create":
      return this.handleCreate(context);
    case "update":
      return this.handleUpdate(context);
    case "delete":
      return this.handleDelete(context, intent);
    case "list":
      return this.handleList(context);
    default:
      return this.error("I didn't understand what you want to do. Try: create, update, delete, or list.");
  }
}

private async handleDelete(context: SkillContext, intent: Intent): Promise<SkillResult> {
  // Confirm before destructive actions
  if (!context.confirmed) {
    return this.confirm(`Are you sure you want to delete "${intent.target}"?`);
  }
  
  await this.invoke("data-store", { action: "delete", id: intent.target });
  return this.success(`Deleted "${intent.target}" successfully.`);
}

Error Handling in Workflows

Per-Step Error Handling


async execute(context: SkillContext): Promise<SkillResult> {
  const workflow = new Workflow("resilient-briefing");

  // Each step has its own error handling
  const weather = await workflow.step("weather", async () => {
    try {
      return await this.invoke("weather-forecast", { city: "Berlin" });
    } catch {
      return { fallback: true, message: "Weather data unavailable" };
    }
  });

  const calendar = await workflow.step("calendar", async () => {
    try {
      return await this.invoke("calendar-check", { range: "today" });
    } catch {
      return { fallback: true, message: "Calendar data unavailable" };
    }
  });

  // Continue even if some steps failed
  return this.respond(`
    Morning briefing:
    Weather: ${weather.fallback ? weather.message : JSON.stringify(weather)}
    Calendar: ${calendar.fallback ? calendar.message : JSON.stringify(calendar)}
  `);
}

Workflow-Level Error Handling


workflow:
  error_strategy: continue       # continue | abort | retry
  max_retries: 2
  retry_delay: 1000
  on_failure:
    action: notify
    message: "Morning briefing partially failed"
    platform: telegram
StrategyBehavior
continueSkip failed steps, continue with available data
abortStop entire workflow on first failure

Data Flow Between Steps

retryRetry failed steps up to max_retries times

Pass data from one step to the next:


async execute(context: SkillContext): Promise<SkillResult> {
  // Step 1: Get user's location
  const location = await this.invoke("location-lookup", {
    query: context.extractParam("city")
  });

  // Step 2: Use location for weather (depends on step 1)
  const weather = await this.invoke("weather-forecast", {
    lat: location.lat,
    lon: location.lon
  });

  // Step 3: Use location for local events (depends on step 1)
  const events = await this.invoke("events-nearby", {
    lat: location.lat,
    lon: location.lon,
    radius: 10
  });

  // Step 4: Combine everything (depends on steps 2 and 3)
  return this.respond(`
    For ${location.name}:
    Weather: ${JSON.stringify(weather)}
    Nearby events: ${JSON.stringify(events)}
  `);
}

Scheduled Workflows

Run workflows on a schedule:


triggers:
  - schedule: "0 7 * * 1-5"      # Weekdays at 7 AM
    output:
      platform: telegram          # Deliver via Telegram
  - schedule: "0 9 * * 1"        # Monday at 9 AM
    output:
      platform: slack
      channel: "#weekly-report"
  - schedule: "*/30 * * * *"     # Every 30 minutes
    output:
      platform: webhook
      url: https://your-app.com/status

Human-in-the-Loop

Some workflows need user confirmation:


async execute(context: SkillContext): Promise<SkillResult> {
  const emails = await this.invoke("email-reader", { unread: true });
  const drafts = await this.generateReplies(emails);

  // Ask user to confirm before sending
  return this.confirm(
    `I've drafted replies to ${drafts.length} emails:\n\n` +
    drafts.map(d => `- To: ${d.to} — "${d.subject}"`).join('\n') +
    `\n\nShall I send them?`,
    {
      onConfirm: async () => {
        await this.invoke("email-sender", { drafts });
        return this.success(`Sent ${drafts.length} replies.`);
      },
      onCancel: () => this.success("No emails sent.")
    }
  );
}

Performance Optimization

TechniqueImpactWhen to Use
Parallel executionHighIndependent steps
CachingHighRepeated API calls
Lazy loadingMediumOptional steps
Result streamingMediumLong-running workflows

Troubleshooting

Workflow Takes Too Long

Early terminationMediumWhen first result is sufficient

Steps Execute in Wrong Order

Ensure dependent steps use await properly. Use workflow.parallel() only for independent steps.

Data Not Passed Between Steps

Check that you're returning data from each step and referencing it correctly in subsequent steps.

Next Steps

Related Articles