Skip to main content

Inside Claude Skills: Custom Modules That Extend Claude

Learn how Claude Skills lets users and developers customize these portable, composable, and code-executable modules to enhance productivity across Claude apps and API integrations.
Nov 10, 2025  · 8 min read

Anthropic recently introduced Claude Skills, a task-specific, reusable module that lets Claude load only the expertise a job requires. Instead of long, repetitive prompts, Skills bundles instructions, scripts, and assets so Claude can produce consistent, brand-aligned outputs across the Claude app, Claude Code, and the API.

In this tutorial, we’ll build an “Auto-Invoice Generator” that turns an Excel timesheet into a client-ready invoice (DOCX/PDF). Along the way, you’ll learn how to:

  • Create a minimal SKILL.md and upload a Skill with supporting assets.
  • Preprocess data like a timesheet with pandas into a predictable JSON payload.
  • Invoke the Skill through the API with a safe tool-use loop (bash/text-editor tags).
  • Capture Claude-generated artifacts (PDF/DOCX)

By the end, you’ll have a portable Skill you can reuse anywhere in the Claude ecosystem.

What Are Claude Skills?

Claude Skills are self-contained task modules that include a SKILL.md (instructions) file, optional scripts, and supporting assets. When a task matches a Skill’s purpose, Claude loads the minimal parts of that Skill to perform the task efficiently. A Skill can be thought of as a packaged expertise you can version, share, and govern across your organization.

Key features of Claude Skills

Claude Skill’s core features explain how they stay fast, reusable, and reliable across the Claude ecosystem. Some of the key features include:

  • Composable: You can combine multiple Skills to run multi-step workflows, such as data cleaning, analysis, and report generation.
  • Portable: Skills follow a common format, so the same Skill works consistently across the Claude ecosystem.
  • Efficient: Claude loads only the minimum components required for the task at hand, which improves both speed and accuracy.
  • Code Executable: Skills can execute code in a secure sandbox, enabling deterministic actions like file creation, parsing, and analytics.

When you ask Claude to “create an Excel budget with monthly roll-ups,” it evaluates your request, scans available Skills, and activates only the relevant pieces. The activation is visible in the reasoning view inside the Claude app. Since Skills are versioned components, teams can roll out updates without changing prompts everywhere.

Using Claude Skills Across the Claude Ecosystem

Whether you’re chatting in Claude apps, coding in Claude Code, or automating workflows via the Developer API, the same Skill can be versioned once, centrally managed, and auto-invoked when the task matches its purpose. This leads to consistent quality and faster turnarounds. Thus, one Skill to reusable everywhere.

In this tutorial, we’ll apply an “Auto-Invoice Generator” Skill across the Claude app and the API to parse an Excel file and output a ready-to-send invoice.

Claude App

Claude Skills live in your Claude App and auto-activate when a request matches their purpose. You enable them once in Settings, then just describe the outcome you want. Claude loads the right Skill and shows it in the reasoning view.

Skill.md with other resources

Source: Claude Blog

To get started:

  • Log in or Sign Up with your Google account and go to Settings.
  • Under Capabilities, search for the Skills tab and upload a Skill( usually a SKILL.md) file that can be used in any chat.

Enabling Skills

Source: Claude Blog

Note: This feature is only available for the Pro, Max, Team, and Enterprise users.

You can also find a list of Skills like algorithmic-art, artifacts-builder, brand-guidelines, canvas-design, internal-comms, mcp-builder, slack-gif-creator, etc, to add in your conversations with Claude.

Click on upload skill and upload a zip file containing the SKILL.md file. Here are the file requirements.

  • ZIP file that includes exactly one SKILL.md file at the root level

  • SKILL.md contains a skill name and description formatted in YAML

The SKILL.md file used in this tutorial looks like this:

name: "auto-invoice-generator-monthly-articles"
description: "Generate monthly invoices for written content from simple line items. Produces a branded PDF or editable DOCX/RTF invoice and, optionally, a one-page timesheet if article titles/links are provided."

Uploading skills

Once you upload the Skill, Claude automatically recognizes it and opens a new chat where it’s ready to use.

New chat with skill enabled

How does this affect usage in chats?

  • You don’t have to “turn on” a Skill inside each conversation. When relevant, Claude auto invokes enabled Skills and shows them in the reasoning view for transparency.
  • Disabling a Skill prevents it from being considered in any chat until you re-enable it.
  • If your organization pins a canonical set of Skills like brand templates, reporting, etc, keeping them enabled ensures consistent outputs every time.

Claude skill in practice

Claude skill in practice

Generated invoice

Next, I passed my own timesheet and asked Claude to build an editable invoice.

Claude skill in practice

Claude spotted the Skill on its own, read the Excel file, and returned an editable Word invoice, which can also be exported to PDF or Word. The invoice layout looks clean, and the subtotal and total are also on point. It adhered to the prompt, and both editable DOCX and formatted PDF files styling matched the Skill’s brand settings. 

Now let’s run the same example using the API.

Claude Developer Platform (API)

Claude skills can also be accessed via the Claude API. In this section, we’ll explore how we can mimic the Claude app interface via the Claude API.

Step 1: Prerequisites

Start by installing all the runtime dependencies:

  • Anthropic for the Claude Messages API

  • pandas and openpyxl to read timesheets from Excel

  • reportlab to generate a fallback PDF invoice locally 

!pip -q install anthropic pandas openpyxl reportlab

Now we have all the dependencies installed.  Next, we can configure our API key.

Step 2: Configure your API key

Before we can call the Messages API, we need to authenticate. This step creates a single, reusable anthropic.Client with the API key. 

import os, json, sys, re
import anthropic
from datetime import datetime, timedelta
API_KEY = "YOUR_ANTHROPIC_API_KEY"
client = anthropic.Client(api_key=API_KEY)

Log in to Anthropic Console and locate the API keys tab under Settings. Click on Create Key and copy your Anthropic API key.

Generating Anthropic API key

Source: Anthropic API

Note: If you are using a private notebook for personal use, then add your API Key. Otherwise, use a secure environment variable so your key isn’t exposed in the notebook or logs.

The above code initializes the Anthropic SDK client and sets up the environment. The client object is then reused for all subsequent Messages API calls.

Step 3: Preprocessing data

In this step, we’ll turn our timesheet.xlsxsheet into a clean, predictable JSON object that the Skill can consume. This keeps downstream logic simple and avoids fragile prompt parsing.

def load_invoice_from_timesheet(excel_path):
    import pandas as pd
    df = pd.read_excel(excel_path)
    df.columns = df.columns.str.strip()  
    invoice_period = "2025-10"
    if 'Date' in df.columns:
        first_date = str(df['Date'].iloc[0])
        date_match = re.search(r'(\d{2})\s+(\w+)\s+(\d{4})', first_date)
        if date_match:
            month_name = date_match.group(2)
            year = date_match.group(3)
            month_num = datetime.strptime(month_name[:3], '%b').month
            invoice_period = f"{year}-{month_num:02d}"   
    article_col = next((col for col in df.columns if 'article' in col.lower() and 'name' in col.lower()), None)
    amount_col = next((col for col in df.columns if 'amount' in col.lower()), None)
    topic_col = next((col for col in df.columns if 'topic' in col.lower()), None)
    line_items = []
    timesheet_articles = []
    
    for idx, row in df.iterrows():
        if pd.isna(row.get(article_col)) or row.get(article_col) == '':
            continue    
        article_name = str(row[article_col]).strip()
        amount = row.get(amount_col, 0) 
        if isinstance(amount, str):
            amount = float(amount.replace('$', '').replace(',', '').strip())
        line_items.append({
            "title": article_name,
            "rate_type": "flat",
            "qty": 1,
            "rate": float(amount)
        })  
        timesheet_articles.append({
            "title": article_name,
            "topic": str(row.get(topic_col, 'N/A')) if topic_col else 'N/A'
        })
    return {
        "client_name": "Client",
        "billing_contact": "billing@example.com",
        "invoice_period": invoice_period,
        "currency": "USD",
        "payment_terms": "Net-30",
        "line_items": line_items,
        "discount_pct": 0.0,
        "tax_pct": 0.0,
        "timesheet": timesheet_articles
    }

The load_invoice_from_timesheet function converts an uploaded Excel file into a normalized invoice JSON payload.

  • It reads the Excel with pandas and normalizes column headers.

  • The code then infers invoice_period from the first Date row (if present) using a regex and month parsing.

  • It finally detects column names for article title, amount, and topic in a case-insensitive manner.

  • This emits two structures:

    • line_items: This is used for billing math (flat rate per article here).

    • timesheet: A flat list of {title, topic} entries for an optional appendix.

  • The regex (\d{2})\s+(\w+)\s+(\d{4}) expects formats like 01 Oct 2025adjust it if your sheet uses another format.

  • Missing columns/values are handled by skipping empty rows, or you can extend the code to fail fast if required.

Our input is now ready. Next, we’ll call the Claude Skill via the API to turn the processed timesheet into an editable invoice.

Step 4: Helper functions

In this section, we define two helper functions that simulate the tool execution that Claude requests during a Skill run. 

def execute_bash_tool(command):
    try:
        dangerous = ['rm -rf /', 'sudo', 'chmod', 'mkfs', '> /dev/']
        if any(d in command for d in dangerous):
            return f"Error: Command blocked for safety: {command}"
        result = subprocess.run(
            command, 
            shell=True, 
            capture_output=True, 
            text=True, 
            timeout=30,
            cwd=tempfile.gettempdir()
        )    
        output = result.stdout if result.stdout else result.stderr
        return output if output else "Command executed successfully"
    except subprocess.TimeoutExpired:
        return "Error: Command timed out"
    except Exception as e:
        return f"Error executing command: {str(e)}"
def execute_text_editor_tool(params):
    try:
        command = params.get('command')
        path = params.get('path')   
        if command == 'create':
            file_text = params.get('file_text', '')
            os.makedirs(os.path.dirname(path) if os.path.dirname(path) else '.', exist_ok=True)
            with open(path, 'w') as f:
                f.write(file_text)
            return f"File created: {path}"
        elif command == 'view':
            if os.path.exists(path):
                with open(path, 'r') as f:
                    content = f.read()
                return content[:1000] 
            return f"Error: File not found: {path}"   
        elif command == 'str_replace':
            if os.path.exists(path):
                with open(path, 'r') as f:
                    content = f.read()
                old_str = params.get('old_str', '')
                new_str = params.get('new_str', '')
                content = content.replace(old_str, new_str)
                with open(path, 'w') as f:
                    f.write(content)
                return f"File updated: {path}"
            return f"Error: File not found: {path}"    
        return f"Unknown command: {command}"
    except Exception as e:
        return f"Error: {str(e)}"

The execute_bash_tool function simulates a safe bash tool for Skill-driven actions. It first blocks dangerous patterns like rm -rf /, sudo, chmod, mkfs, etc, as a safety gate and runs the command with subprocess.run() in the OS temp directory with an enforced 30-second timeout.

The execute_text_editor_tool function provides a minimal text-editing interface used by Skills. It supports three commands, namely create (to write a new file with file_text), view (to return up to 1,000 chars of a file), and str_replace ( for new_str in place). It also auto-creates parent folders for create, checks file existence for view and str_replace, and writes updates back to disk.

These helpers let you complete a tool-use loop locally with guardrails. Now, Skill can request file edits or shell actions during invoice generation without risking your system.

Step 5: Invoke the Skill via API 

This step drives the end-to-end invoice generation through the Claude Messages API. It sends a structured request, enables tool use, iterates through any tool calls Claude requests, and finally collects any generated PDF files from the working and temp directories.

def generate_invoice_with_claude(invoice): 
    user_text = f"""Generate a professional PDF invoice with the following data:
Client: {invoice['client_name']}
Period: {invoice['invoice_period']}
Currency: {invoice['currency']}
Payment Terms: {invoice['payment_terms']}
Line Items:
{json.dumps(invoice['line_items'], indent=2)}
Timesheet Articles:
{json.dumps(invoice['timesheet'], indent=2)}
Please create a professional PDF invoice with:
1. Invoice header with invoice number (INV-{invoice['invoice_period'].replace('-', '')}-001)
2. Client billing information
3. Line items table with amounts
4. Subtotal and total calculations
5. Timesheet section showing all articles and topics
Save the PDF as: invoice_{invoice['client_name'].lower()}_{invoice['invoice_period']}.pdf
""" 
    tools = [
        {"type": "bash_20250124", "name": "bash"},
        {"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}
    ]
    messages = [{"role": "user", "content": user_text}]
    iteration = 0
    max_iterations = 15   
    while iteration < max_iterations:
        iteration += 1
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=8192,
            tools=tools,
            messages=messages
        )   
        messages.append({"role": "assistant", "content": response.content}) 
        if response.stop_reason == "end_turn":
            break     
        if response.stop_reason == "tool_use":
            tool_results = []    
            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    if tool_name == "bash":
                        result = execute_bash_tool(tool_input.get('command', ''))
                    elif tool_name == "str_replace_based_edit_tool":
                        result = execute_text_editor_tool(tool_input)
                    else:
                        result = f"Unknown tool: {tool_name}" 
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
            messages.append({"role": "user", "content": tool_results})
        else:
            break
    pdf_files = []
    for file in os.listdir('.'):
        if file.endswith('.pdf') and 'invoice' in file.lower():
            pdf_files.append(file)
    for file in os.listdir(tempfile.gettempdir()):
        if file.endswith('.pdf') and 'invoice' in file.lower():
            temp_path = os.path.join(tempfile.gettempdir(), file)
            import shutil
            dest_path = os.path.join('.', file)
            shutil.copy2(temp_path, dest_path)
            pdf_files.append(file)   
    return pdf_files
def main():
    if len(sys.argv) < 2:
        print("Usage: python app.py <timesheet.xlsx>")
        sys.exit(1)    
    excel_file = sys.argv[1]  
    if not os.path.exists(excel_file):
        print(f"Error: File not found: {excel_file}")
        sys.exit(1)    
    try:
        invoice = load_invoice_from_timesheet(excel_file)
        pdf_files = generate_invoice_with_claude(invoice)    
        if pdf_files:
            for pdf in pdf_files:
                print(f"Invoice generated: {os.path.abspath(pdf)}")
        else:
            print("Error: No PDF file was generated.")     
    except Exception as e:
        print(f"Error: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
if __name__ == "__main__":
    main()

The generate_invoice_with_claude function sends the normalized invoice to the Claude API and drives Skill execution end-to-end. Here is what the code does:

  • It builds a clear user_text that embeds client, period, currency, terms, line items, and timesheet entries, plus explicit output requirements like header, tables, totals, and filename.

  • Then it enables tool execution by declaring a bash_number and text_editor_number with a tool-aware message loop that posts the request, inspects stop_reason, and when tool_use is requested, it dispatches to local helpers and returns tool_result blocks to continue the exchange.

  • Finally, it stops on end_turn or when the loop limit is reached, then scans the working directory and the OS temp directory for files matching invoice*.pdf. It copies any temp artifacts back to the project folder and returns the list of discovered PDFs.

  • The main function provides a simple CLI command: python app.py <timesheet.xlsx>, which validates the input path, then runs the pipeline: load_invoice_from_timesheet(...) to build the payload, and generate_invoice_with_claude(...)  to invoke the Skill and collect PDFs.

With this in place, the API call should produce a client-ready PDF. If no file appears, review the tool_result logs and Skill configuration, then retry with a small test sheet.

Claude skill in practice

Conclusion

You now have a working Auto-Invoice Generator powered by Claude Skills, and the API is capable of turning raw spreadsheets into client-ready invoices. This demo showcases what Skills do best, i.e, portable setup, auto-activation, deterministic code execution, and consistent results across the app and API without recreating prompts in every thread. Claude Skills returns consistent outputs across contexts, which is reusable domain knowledge and code-backed. Though Skills can execute code, you still must enforce security by enabling only trusted Skills, using org-level controls, and reviewing updates before rollout.

To go further, explore Anthropic’s Skills docs and the Console for versioning and upgrades, then start composing multiple Skills to build end-to-end workflows.

Introduction to Claude Models

Learn how to work with Claude using the Anthropic API to solve real-world tasks and build AI-powered applications.
Explore Course

Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

I am a Google Developers Expert in ML(Gen AI), a Kaggle 3x Expert, and a Women Techmakers Ambassador with 3+ years of experience in tech. I co-founded a health-tech startup in 2020 and am pursuing a master's in computer science at Georgia Tech, specializing in machine learning.

Topics

Learn with DataCamp

Course

Large Language Models (LLMs) Concepts

2 hr
74.7K
Discover the full potential of LLMs with our conceptual course covering LLM applications, training methodologies, ethical considerations, and latest research.
See DetailsRight Arrow
Start Course
See MoreRight Arrow
Related

Tutorial

Imagine with Claude: A Guide With Practical Examples

Learn how Anthropic's Imagine with Claude introduces a new paradigm for AI-assisted software development, generating functionality on the fly.
François Aubry's photo

François Aubry

Tutorial

Claude Opus 4 with Claude Code: A Guide With Demo Project

Plan, build, test, and deploy a machine learning project from scratch using the Claude Opus 4 model with Claude Code.
Abid Ali Awan's photo

Abid Ali Awan

Tutorial

Claude Agent SDK Tutorial: Create Agents Using Claude Sonnet 4.5

Learn how the Claude Agent SDK works by building three projects, from one-shot to custom-tool agents.
Abid Ali Awan's photo

Abid Ali Awan

Tutorial

Claude Code: A Guide With Practical Examples

Learn how to use Anthropic's Claude Code to improve software development workflows through a practical example using the Supabase Python library.
Aashi Dutt's photo

Aashi Dutt

Tutorial

Claude for Chrome: AI-Powered Browser Assistance and Automation

Learn how to install and use Claude for Chrome, Anthropic’s AI-powered browser assistant. Discover how it automates form filling, email cleanup, and web tasks, plus key safety tips and current limitations.
Bex Tuychiev's photo

Bex Tuychiev

code-along

Introduction to Claude

Aimée, a Learning Solutions Architect at DataCamp, takes you through how to use Claude. You'll get prompt engineering tips, see a data analysis workflow, and learn how to generate Python and SQL code with Claude.
Aimée Gott's photo

Aimée Gott

See MoreSee More