How to Create Your First OpenClaw Skill: A Step-by-Step Guide
How to Create Your First OpenClaw Skill: A Step-by-Step Guide
Published: March 13, 2026
Reading Time: 12 minutes
Category: Tutorial
Difficulty: Beginner to Intermediate
OpenClaw's Skill System is what sets it apart from other AI assistants. Skills allow you to create custom automation workflows that integrate AI with your daily tasks.
In this tutorial, we'll build a practical Daily Standup Reporter skill that:
- Fetches your Git commits from the last 24 hours
- Reads your calendar events
- Generates a formatted standup report
- Sends it to Slack
Let's get started!
📋 Prerequisites
Before creating your first skill, ensure you have:
- [ ] OpenClaw installed (Setup Guide)
- [ ] Basic understanding of JavaScript/TypeScript
- [ ] A text editor (VS Code recommended)
- [ ] Terminal/Command line familiarity
🏗️ Understanding OpenClaw Skills
What is a Skill?
A Skill in OpenClaw is a reusable automation module that:
- Defines inputs (parameters)
- Executes a workflow
- Returns outputs (results)
- Can be triggered by commands, schedules, or events
Skill Structure
my-skill/
├── skill.json # Skill metadata
├── index.ts # Main entry point
├── actions/
│ └── main.ts # Core logic
├── utils/
│ └── helpers.ts # Helper functions
└── tests/
└── skill.test.ts # Unit tests
🛠️ Step 1: Create Your Skill Directory
Open your terminal and run:
# Navigate to OpenClaw skills directory
cd ~/.openclaw/skills
# Create new skill directory
mkdir daily-standup-reporter
cd daily-standup-reporter
# Initialize npm project
npm init -y
# Install dependencies
npm install @openclaw/core @openclaw/git @openclaw/calendar @openclaw/slack
npm install -D typescript @types/node
# Initialize TypeScript
npx tsc --init
📝 Step 2: Define Skill Metadata
Create skill.json:
{
"name": "daily-standup-reporter",
"version": "1.0.0",
"description": "Automatically generates daily standup reports from Git commits and calendar events",
"author": "Your Name",
"license": "MIT",
"keywords": ["standup", "git", "calendar", "automation", "productivity"],
"inputs": {
"dateRange": {
"type": "string",
"description": "Date range for report (e.g., '1d' for last day)",
"default": "1d"
},
"slackChannel": {
"type": "string",
"description": "Slack channel to post report",
"required": true
},
"includeCalendar": {
"type": "boolean",
"description": "Include calendar events in report",
"default": true
}
},
"outputs": {
"report": {
"type": "string",
"description": "Generated standup report"
},
"commitCount": {
"type": "number",
"description": "Number of commits included"
}
},
"permissions": [
"git:read",
"calendar:read",
"slack:write"
]
}
💻 Step 3: Write the Main Logic
Create src/index.ts:
import { Skill, SkillContext } from '@openclaw/core';
import { GitClient } from '@openclaw/git';
import { CalendarClient } from '@openclaw/calendar';
import { SlackClient } from '@openclaw/slack';
interface StandupInputs {
dateRange: string;
slackChannel: string;
includeCalendar: boolean;
}
interface StandupOutputs {
report: string;
commitCount: number;
}
export default class DailyStandupReporter implements Skill<StandupInputs, StandupOutputs> {
async execute(context: SkillContext, inputs: StandupInputs): Promise<StandupOutputs> {
const { dateRange, slackChannel, includeCalendar } = inputs;
// Initialize clients
const git = new GitClient();
const calendar = new CalendarClient();
const slack = new SlackClient();
try {
// Fetch Git commits
context.log('Fetching Git commits...');
const commits = await git.getCommits({ since: dateRange });
// Fetch calendar events
let events = [];
if (includeCalendar) {
context.log('Fetching calendar events...');
events = await calendar.getEvents({ range: dateRange });
}
// Generate report
context.log('Generating standup report...');
const report = this.generateReport(commits, events);
// Send to Slack
context.log('Sending to Slack...');
await slack.postMessage({
channel: slackChannel,
text: report,
blocks: this.formatSlackBlocks(commits, events)
});
context.log('Standup report sent successfully!');
return {
report,
commitCount: commits.length
};
} catch (error) {
context.error('Failed to generate standup report:', error);
throw error;
}
}
private generateReport(commits: any[], events: any[]): string {
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
let report = `📝 Daily Standup Report - ${today}\n\n`;
// Yesterday's work
report += `*Yesterday's Work:*\n`;
if (commits.length > 0) {
commits.forEach(commit => {
report += `• ${commit.message} (${commit.repository})\n`;
});
} else {
report += `• No commits recorded\n`;
}
// Today's focus
report += `\n*Today's Focus:*\n`;
if (events.length > 0) {
events.forEach(event => {
report += `• ${event.title} at ${event.time}\n`;
});
} else {
report += `• Continue current tasks\n`;
}
// Blockers
report += `\n*Blockers:*\n• None reported\n`;
return report;
}
private formatSlackBlocks(commits: any[], events: any[]): any[] {
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: '📝 Daily Standup Report',
emoji: true
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Date:* ${new Date().toLocaleDateString()}`
}
},
{
type: 'divider'
}
];
// Add commits section
if (commits.length > 0) {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: `*Yesterday's Work* (${commits.length} commits)`
}
});
commits.slice(0, 5).forEach(commit => {
blocks.push({
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `• \`${commit.hash.substring(0, 7)}\` ${commit.message}`
}
]
});
});
if (commits.length > 5) {
blocks.push({
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `_... and ${commits.length - 5} more commits_`
}
]
});
}
}
return blocks;
}
}
🔧 Step 4: Build and Test
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Add build script to package.json:
{
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest"
}
}
Build your skill:
npm run build
🧪 Step 5: Test Your Skill
Create src/__tests__/skill.test.ts:
import DailyStandupReporter from '../index';
import { SkillContext } from '@openclaw/core';
describe('DailyStandupReporter', () => {
let skill: DailyStandupReporter;
let mockContext: SkillContext;
beforeEach(() => {
skill = new DailyStandupReporter();
mockContext = {
log: jest.fn(),
error: jest.fn(),
config: {}
} as any;
});
it('should generate report with commits and events', async () => {
const inputs = {
dateRange: '1d',
slackChannel: '#standup',
includeCalendar: true
};
// Mock the API calls
jest.spyOn(skill as any, 'generateReport').mockReturnValue('Mock report');
const result = await skill.execute(mockContext, inputs);
expect(result).toHaveProperty('report');
expect(result).toHaveProperty('commitCount');
expect(mockContext.log).toHaveBeenCalled();
});
});
Run tests:
npm test
🚀 Step 6: Install and Use
Install to OpenClaw
# From skill directory
openclaw skills install .
# Or from GitHub
openclaw skills install github:yourusername/daily-standup-reporter
Configure Environment Variables
Add to your ~/.openclaw/.env:
# Git provider token
GITHUB_TOKEN=your_github_token
# Calendar API
GOOGLE_CALENDAR_CREDENTIALS=path/to/credentials.json
# Slack
SLACK_BOT_TOKEN=xoxb-your-bot-token
Use Your Skill
# Command line
openclaw run daily-standup-reporter --slackChannel="#standup"
# With options
openclaw run daily-standup-reporter \
--dateRange="2d" \
--slackChannel="#engineering" \
--includeCalendar=true
Schedule Automation
Add to ~/.openclaw/crontab:
# Run every weekday at 9:00 AM
0 9 * * 1-5 openclaw run daily-standup-reporter --slackChannel="#standup"
🎓 Best Practices
1. Error Handling
Always wrap external API calls in try-catch:
try {
const data = await api.call();
} catch (error) {
context.error('API call failed:', error);
// Provide fallback or graceful degradation
}
2. Logging
Use context logging for debugging:
context.log('Starting operation...');
context.warn('Deprecated feature used');
context.error('Something went wrong');
3. Input Validation
Validate inputs early:
if (!inputs.slackChannel) {
throw new Error('slackChannel is required');
}
4. Documentation
Document your skill's inputs and outputs in skill.json.
📚 Next Steps
Now that you've created your first skill, explore:
- 5 Best OpenClaw Skills for Developers
- OpenClaw Security Best Practices
- OpenClaw API Integration Guide
🔗 Share Your Skill
Contribute to the community:
- Push to GitHub
- Add
#openclaw-skilltopic - Submit to OpenClaw Skills Registry
❓ Troubleshooting
"Module not found" error
# Rebuild the skill
npm run build
# Check dependencies are installed
npm install
Permission denied
# Grant permissions in OpenClaw settings
openclaw config permissions add daily-standup-reporter
API authentication failed
Verify your environment variables are set correctly:
openclaw env check
🎉 Congratulations!
You've created your first OpenClaw skill! This Daily Standup Reporter demonstrates:
- ✅ Skill structure and metadata
- ✅ API integrations (Git, Calendar, Slack)
- ✅ Report generation and formatting
- ✅ Error handling and logging
- ✅ Testing and validation
What will you build next?
Last updated: March 13, 2026
Questions? Join our Discord community or check the documentation.