tutorialskillsdevelopmentautomationbeginner

How to Create Your First OpenClaw Skill: A Step-by-Step Guide

7 min read

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:


🔗 Share Your Skill

Contribute to the community:

  1. Push to GitHub
  2. Add #openclaw-skill topic
  3. 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.

Enjoyed this article? Share it with others!