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:
- Call an external API
- Invoke another skill
- Process data
- Make decisions
- Wait for conditions
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 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
| Strategy | Behavior |
|---|
continue | Skip failed steps, continue with available data |
|---|
abort | Stop entire workflow on first failure |
|---|
retry | Retry 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
| Technique | Impact | When to Use |
|---|
| Parallel execution | High | Independent steps |
|---|
| Caching | High | Repeated API calls |
|---|
| Lazy loading | Medium | Optional steps |
|---|
| Result streaming | Medium | Long-running workflows |
|---|
| Early termination | Medium | When first result is sufficient |
|---|
- Identify slow steps:
openclaw skills test my-skill --input "..." --trace. - Run independent steps in parallel.
- Add caching for API calls.
- Increase timeout:
runtime.timeout: 30000.
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
- Master prompt techniques: Using Prompts Inside Skills: Tips and Techniques.
- Handle dependencies: Skill Dependencies: Managing Libraries and APIs.
- Publish your workflow: Publishing Your Skill to the Community Skill Registry.
Related Articles
- Multi-Agent OpenClaw: Running Multiple Assistants — Configure and manage multiple OpenClaw agents working independently or collaboratively.
- Resolving Conflicts Between OpenClaw Skills — Fix skill conflicts and priority issues when multiple OpenClaw skills compete for the same triggers.
- Build a Daily Routine Assistant with OpenClaw — Step-by-step tutorial for building an AI-powered daily routine assistant using OpenClaw skills.
- Microsoft AutoGen — Multi-Agent Conversation Patterns Done Right — By 2026, the novelty of single-agent workflows has worn off. We've mastered chaining LLMs and building basic RAG pipelines. The frontier has moved to coordination. Getting multiple specialized AI agents to collaborate effectively on a compl
- Advanced Prompt Techniques: Chain-of-Thought and ReAct — Apply advanced prompting strategies like chain-of-thought reasoning and ReAct for complex problem-solving.