Try an interactive version of this dialog: Sign up at solve.it.com, click Upload, and pass this URL.

Note: 60

Agent Skills

In this dialog we will implement the Agent Skills specification on top of claudette.

Note: 4

Skill discovery

Prompt: 48

Let's begin by implementing skill discovery. Can you paste in a new Note cell the relevant quote from the spec about what skills look like and how they are discovered?

Assistant: 2,067

Copied!
{
  "id": "toolu_01QkBRcMCiUxWZ365QaXyEDP",
  "call": {
    "function": "read_url",
    "arguments": {
      "url": "https://agentskills.io/home"
    }
  },
  "result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nOverview\\n\\n  * [Overview](/home)\\n\\n\\n  * [What are skills?](/what-are-skills)\\n\\n\\n  * [Specification](/specification)\\n\\n\\n  * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n  * Why Agent Skills?\\n  * What can Agent Skills enable?\\n  * Adoption\\n  * Open development\\n  * Get started\\n\\n\\n\\n# Overview\\n\\nCo<TRUNCATED>"
}

Let me also get the specification page which has the details on skill structure and discovery:

Copied!
{
  "id": "toolu_01EgU7W4FKLA8tsSbARTsHZD",
  "call": {
    "function": "read_url",
    "arguments": {
      "url": "https://agentskills.io/specification"
    }
  },
  "result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nSpecification\\n\\n  * [Overview](/home)\\n\\n\\n  * [What are skills?](/what-are-skills)\\n\\n\\n  * [Specification](/specification)\\n\\n\\n  * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n  * Directory structure\\n  * SKILL.md format\\n  * Frontmatter (required)\\n  * name field\\n  * description field\\n  * license fie<TRUNCATED>"
}

Now let me get the "Integrate skills" page which should have the discovery details:

Copied!
{
  "id": "toolu_01N8vNsmx9qh3CEW2U3W3SFJ",
  "call": {
    "function": "read_url",
    "arguments": {
      "url": "https://agentskills.io/integrate-skills"
    }
  },
  "result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nIntegrate skills into your agent\\n\\n  * [Overview](/home)\\n\\n\\n  * [What are skills?](/what-are-skills)\\n\\n\\n  * [Specification](/specification)\\n\\n\\n  * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n  * Integration approaches\\n  * Overview\\n  * Skill discovery\\n  * Loading metadata\\n  * Parsing frontmatter\\<TRUNCATED>"
}

Now I have the relevant information. Let me create a note with the key quotes about skill structure and discovery:

Copied!
{
  "id": "toolu_015mWHuu73DknWmuGgjhbdDv",
  "call": {
    "function": "add_msg",
    "arguments": {
      "content": "## Agent Skills: Structure and Discovery\n\n### What is a Skill?\n\nA skill is a directory containing at minimum a `SKILL.md` file:\n\n```\nskill-name/\n\u2514\u2500\u2500 SKILL.md          # Required\n```\n\nThe `SKILL.md` file must contain YAML frontmatter followed by Markdown content:\n\n```yaml\n---\nname: skill-name\ndescription: A description of what this skill does and when to use it.\n---\n```\n\n### Skill Discovery\n\nFrom the [Integrate Skills](https://agentskills.io/integrate-skills) page:\n\n> Skills are folders containing a `SKILL.md` file. Your agent should scan configured directories for valid skills.\n\n### Loading Me<TRUNCATED>"
    }
  },
  "result": "'_9cfeea5f'"
}

Done! I've created a note summarising the key parts of the spec regarding:

  1. Skill structure - A folder with a SKILL.md file containing YAML frontmatter (name + description) followed by markdown instructions
  2. Discovery - Scan configured directories for folders containing SKILL.md files
  3. Metadata loading - Parse only frontmatter at startup to keep context usage low
  4. Context injection - Use XML format with <available_skills> containing skill name, description, and location

What would you like to tackle first for the implementation?

Note: 457

Agent Skills: Structure and Discovery

What is a Skill?

A skill is a directory containing at minimum a SKILL.md file:

Copied!
skill-name/
└── SKILL.md          # Required

The SKILL.md file must contain YAML frontmatter followed by Markdown content:

Copied!
---
name: skill-name
description: A description of what this skill does and when to use it.
---

Skill Discovery

From the Integrate Skills page:

Skills are folders containing a SKILL.md file. Your agent should scan configured directories for valid skills.

Loading Metadata

At startup, parse only the frontmatter of each SKILL.md file. This keeps initial context usage low.

Injecting into Context

For Claude models, the recommended format uses XML:

Copied!
<available_skills>
  <skill>
    <name>pdf-processing</name>
    <description>Extracts text and tables from PDF files, fills forms, merges documents.</description>
    <location>/path/to/skills/pdf-processing/SKILL.md</location>
  </skill>
</available_skills>

For filesystem-based agents, include the location field with the absolute path to the SKILL.md file. For tool-based agents, the location can be omitted. Keep metadata concise. Each skill should add roughly 50-100 tokens to the context.

Code: 9 ()

!pip install python-frontmatter

Code: 4 ()

import frontmatter

Code: 61 ()

with open('/app/data/skills/skills/pdf/SKILL.md', 'r') as f:
    post = frontmatter.load(f)
    display(post['name'])
    display(post['description'])

Output: 124

'pdf'
'Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.'

Code: 21 ()

import os
from dataclasses import dataclass
from typing import List

Code: 229 ()

# A colon-separated list of directories containing skills
os.environ['SKILLS_PATH'] = '/app/data/skills/skills'

@dataclass
class Skill:
    name: str
    description: str
    license: str = None

    @classmethod
    def from_directory(cls, directory):
        post = frontmatter.load(os.path.join(directory, 'SKILL.md'))
        return cls(**post)

def load_skills() -> List[Skill]:
    return [
        Skill.from_directory(full_path)
        for skills_root in os.environ.get('SKILLS_PATH', []).split(":")
        for directory in os.listdir(skills_root)
        if os.path.isdir(full_path := os.path.join(skills_root, directory))
    ]

load_skills()

Prompt: 61

We are still missing some other possible keys, but let's move on for now. Now please add a cell with the relevant quote of the spec that describes how to insert the skill list into the system prompt.

Assistant: 964

Copied!
{
  "id": "toolu_01JKLL6qEHQ7gEZd8i9kVKAd",
  "call": {
    "function": "read_url",
    "arguments": {
      "url": "https://agentskills.io/integrate-skills"
    }
  },
  "result": "'Skip to main content\\n\\n[Agent Skills home pageAgent Skills](/)\\n\\nSearch...\\n\\n\u2318KAsk AI\\n\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n  * [agentskills/agentskills](https://github.com/agentskills/agentskills)\\n\\n\\n\\nSearch...\\n\\nNavigation\\n\\nIntegrate skills into your agent\\n\\n  * [Overview](/home)\\n\\n\\n  * [What are skills?](/what-are-skills)\\n\\n\\n  * [Specification](/specification)\\n\\n\\n  * [Integrate skills](/integrate-skills)\\n\\n\\n\\nOn this page\\n\\n  * Integration approaches\\n  * Overview\\n  * Skill discovery\\n  * Loading metadata\\n  * Parsing frontmatter\\<TRUNCATED>"
}
Copied!
{
  "id": "toolu_0157v464CZhBw4dqhBjDMfZq",
  "call": {
    "function": "add_msg",
    "arguments": {
      "content": "## Injecting Skills into System Prompt\n\nFrom the [Integrate Skills](https://agentskills.io/integrate-skills) spec:\n\n> Include skill metadata in the system prompt so the model knows what skills are available. Follow your platform's guidance for system prompt updates. For example, for Claude models, the recommended format uses XML:\n\n```xml\n<available_skills>\n  <skill>\n    <name>pdf-processing</name>\n    <description>Extracts text and tables from PDF files, fills forms, merges documents.</description>\n    <location>/path/to/skills/pdf-processing/SKILL.md</location>\n  </skill>\n  <skill>\n    <name><TRUNCATED>"
    }
  },
  "result": "'_b4d031bd'"
}

Done! I've added a note with the spec's guidance on injecting skills into the system prompt. The key points are:

  • Use XML format with <available_skills> wrapper
  • Each skill gets <name>, <description>, and optionally <location>
  • Include location for filesystem-based agents (like ours)
  • Keep it concise: ~50-100 tokens per skill

Ready to implement the to_prompt() method on your Skill class?

Note: 4

Skill description

Note: 370

Injecting Skills into System Prompt

From the Integrate Skills spec:

Include skill metadata in the system prompt so the model knows what skills are available. Follow your platform's guidance for system prompt updates. For example, for Claude models, the recommended format uses XML:

Copied!
<available_skills>
  <skill>
    <name>pdf-processing</name>
    <description>Extracts text and tables from PDF files, fills forms, merges documents.</description>
    <location>/path/to/skills/pdf-processing/SKILL.md</location>
  </skill>
  <skill>
    <name>data-analysis</name>
    <description>Analyzes datasets, generates charts, and creates summary reports.</description>
    <location>/path/to/skills/data-analysis/SKILL.md</location>
  </skill>
</available_skills>

For filesystem-based agents, include the location field with the absolute path to the SKILL.md file. For tool-based agents, the location can be omitted. Keep metadata concise. Each skill should add roughly 50-100 tokens to the context.

Prompt: 25

Is there a way to add identation to the output of ElementTree's tostring?

Assistant: 171

Yes! Since Python 3.9, ElementTree.tostring() and ElementTree.indent() support pretty-printing:

Copied!
from xml.etree import ElementTree as ET

# Create your element tree, then indent it before converting to string:
ET.indent(root)  # modifies tree in-place
xml_str = ET.tostring(root, encoding='unicode')

You can also specify the indentation level with ET.indent(root, space=" ") for 2 spaces, or space=" " for 4.

Note: 7

Tools for skill usage

Note: 169

Great! I think that does it for discovery. Next I want to implement some tools so claudette can actually use the skills:

  • skill(name) will "activate" a skill (return the contents of the SKILL.md)
  • read(path) will return the contents of a file on disk.
  • script(path) will run the specified skill-bundled python script and return the output.

Here is a reference for how Claude Code consumes skills: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview

Prompt: 18

Does that seem like a sufficient toolset to get started?

Assistant: 214

Yes, that's a solid starting set! Those three tools cover the core workflow:

  1. skill(name) — Load the full instructions when a skill is relevant
  2. read(path) — Access supporting files (examples, templates, configs) referenced in the skill
  3. script(path) — Execute bundled utilities (e.g. PDF processing, spreadsheet manipulation)

One thing you might want to add later is a write(path, content) tool — many skills (like docx, pdf, xlsx) will need to create output files. But you can add that when you hit that need.

Want to start implementing skill(name) first?

Note: 93 ()

Tool example from claudette's documentation:

Copied!
def sums(
    a:int,  # First thing to sum
    b:int=1 # Second thing to sum
) -> int: # The sum of the inputs
    "Adds a + b."
    return a + b

Prompt: 57

Please add a message with the relevant snippet from https://mikhail.io/2025/10/claude-code-skills/ that explains what Anthropic's skill tool description looks like.

Assistant: 910

Copied!
{
  "id": "toolu_01VLuRRRr4hToDJkG5Fei8L1",
  "call": {
    "function": "read_url",
    "arguments": {
      "url": "https://mikhail.io/2025/10/claude-code-skills/"
    }
  },
  "result": "'[ ![Mikhail Shilkov](https://mikhail.io/images/author.jpg) Mikhail Shilkov ](/)\\n\\n  * [TOPICS](/tags/)\\n  * [ARCHIVES](/archives/)\\n  * [TALKS](/talks/)\\n  * [ABOUT](/about/)\\n  * [__](https://mikhail.io/feed/)\\n  * [__](https://x.com/MikhailShilkov)\\n  * [__](https://dev.to/mikhailshilkov)\\n  * [__](https://medium.com/@MikhailShilkov)\\n  * [__](https://github.com/MikhailShilkov)\\n  * [__](https://www.linkedin.com/in/MikhailShilkov)\\n\\n\\n\\n  * __\\n\\n  * [ __](h<TRUNCATED>"
}
Copied!
{
  "id": "toolu_011UUKrhdRX4Cu8qoZQQWK3i",
  "call": {
    "function": "add_msg",
    "arguments": {
      "content": "### Claude Code's Skill Tool Definition\n\nFrom [Mikhail Shilkov's reverse-engineering of Claude Code](https://mikhail.io/2025/10/claude-code-skills/):\n\n```\n## Skill\n\n**Input Schema:**\n\nobject:\n  - command (required):\n    string\n      # The skill name (no arguments). E.g., \"pdf\" or \"xlsx\"\n\n**Description:**\n\nExecute a skill within the main conversation\n\n<skills_instructions>\nWhen users ask you to perform tasks, check if any of the available skills\nbelow can help complete the task more effectively. Skills provide specialized\ncapabilities and domain knowledge.\n\nHow to use skills:\n- Invoke skills us<TRUNCATED>"
    }
  },
  "result": "'_f52feb63'"
}

Done! I've added a note with the key snippet showing Claude Code's Skill tool definition.

Interesting design choice: they embed the <available_skills> XML inside the tool's description field rather than in the system prompt. This keeps the system prompt lean and bundles everything skill-related in one place.

Note: 517

Claude Code's Skill Tool Definition

From Mikhail Shilkov's reverse-engineering of Claude Code:

Copied!
## Skill

**Input Schema:**

object:
  - command (required):
    string
      # The skill name (no arguments). E.g., "pdf" or "xlsx"

**Description:**

Execute a skill within the main conversation

<skills_instructions>
When users ask you to perform tasks, check if any of the available skills
below can help complete the task more effectively. Skills provide specialized
capabilities and domain knowledge.

How to use skills:
- Invoke skills using this tool with the skill name only (no arguments)
- When you invoke a skill, you will see <command-message>The "{name}" skill is loading</command-message>
- The skill's prompt will expand and provide detailed instructions on how to complete the task
- Examples:
  - `command: "pdf"` - invoke the pdf skill
  - `command: "xlsx"` - invoke the xlsx skill
  - `command: "ms-office-suite:pdf"` - invoke using fully qualified name

Important:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already running
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
</skills_instructions>

<available_skills>
[Skills are listed here - see next section]
</available_skills>

Note: Claude Code embeds the <available_skills> XML directly in the tool description, rather than in the system prompt.

Code: 474 ()

from IPython.display import Markdown

@dataclass
class Skill:
    name: str
    description: str

    location: str = None

    @classmethod
    def from_directory(cls, directory):
        location = os.path.join(directory, 'SKILL.md')
        post = frontmatter.load(location)
        return cls(name=post['name'], description=post['description'], location=location)

    def instructions(self):
        with open(self.location) as f:
            return f.read()

def skill(
    name: str, # The name of a skill to activate
) -> str: # The contents of the skill's SKILL.MD
    for skills_root in os.environ.get('SKILLS_PATH', '').split(":"):
        skill_path = os.path.join(skills_root, name)
        if os.path.exists(skill_path):
            return Skill.from_directory(skill_path).instructions()
skill.__doc__ =     f"""
Execute a skill within the main conversation

When users ask you to perform tasks, check if any of the available skills below
can help complete the task more effectively. Skills provide specialized
capabilities and domain knowledge.

How to use skills:
- Invoke skill using this tool with the skill name only (no arguments).
- When you invoke a skill, the skill's prompt will expand and provide
    detailed instructions on how to complete the task.

Important
- Only use skills listed in <available_skills> below.
- Do not invoke a skill that is already running.

{available_skills()}
"""

Markdown(skill('pdf'))

Code: 222 ()

from pathlib import Path

def is_valid_absolute_path(
    path: str
) -> bool:
    path = Path(path)

    if not path.is_absolute():
        return False
    
    valid_roots = [Path(root) for root in os.environ.get('SKILLS_PATH', '').split(":")]

    if not any(path.is_relative_to(root) for root in valid_roots):
        return False
    
    return True

p = '/app/data/skills/skills/pdf/forms.md'
display(p, is_valid_absolute_path(p))

p = '~/skills/skills/pdf/forms.md'
display(p, is_valid_absolute_path(p))

p = '/app/data/skills/README.md'
display(p, is_valid_absolute_path(p))

Output: 160

'/app/data/skills/skills/pdf/forms.md'
True
'~/skills/skills/pdf/forms.md'
False
'/app/data/skills/README.md'
False

Code: 163 ()

def read(
    absolute_path: str # Absolute path of the file to read
) -> str: # Text contents of the read file
    """
    Returns the text contents of the file at the given absolute path
    """

    if not is_valid_absolute_path(absolute_path):
        raise ValueError("The path is not absolute, or the agent is not authorized to read it")

    with open(absolute_path, 'r') as f:
        return f.read()

read('/app/data/skills/skills/pdf/forms.md')

Prompt: 34

How can I run a Python script given a path? What are my options, and what is most idiomatic?

Assistant: 309

The main options:

  1. subprocess.run() — The modern, idiomatic choice (Python 3.5+). Captures output, handles errors, supports timeouts and working directory.

  2. os.system() — Simple but limited. Only returns exit code, no output capture.

  3. runpy.run_path() — Runs script in current interpreter and returns its globals. Useful for dynamic module loading, but less isolation.

For your script() tool, subprocess.run() is the way to go since you want to:

  • Capture stdout/stderr to return to the agent
  • Run scripts in isolation
  • Potentially set the working directory to the skill's folder

Basic pattern:

Copied!
import subprocess
result = subprocess.run(['python', script_path], capture_output=True, text=True)
# result.stdout, result.stderr, result.returncode

Want to implement script() yourself, or would you like me to sketch it out?

Prompt: 9

What does text=True do?

Assistant: 136

By default, subprocess.run() returns stdout and stderr as bytes. Setting text=True decodes them to strings (using the system's default encoding, usually UTF-8).

Copied!
# text=False (default): result.stdout is b'Hello\n'
# text=True:            result.stdout is 'Hello\n'

It's equivalent to the older universal_newlines=True parameter.

Prompt: 70

What does the output of claude code's Read and Bash tools look like? Is there a specific format? Search the web to make sure.

I'm particularly curious about how the Bash tool handles stdout vs stderr, and the return code.

Assistant: 637

Found it. The code execution tool (the API version) has a very clear format:

* The bash code execution tool result includes separate stdout, stderr, and return_code fields:

Copied!
{
  "type": "bash_code_execution_tool_result",
  "tool_use_id": "srvtoolu_01B3C4D5E6F7G8H9I0J1K2L3",
  "content": {
    "type": "bash_code_execution_result",
    "stdout": "total 24\ndrwxr-xr-x 2 user user 4096 Jan 1 12:00 .\n...",
    "stderr": "",
    "return_code": 0
  }
}

Where return_code is 0 for success, non-zero for failure.

For Claude Code's Bash tool (the CLI tool), it's a bit different. * The implementation concatenates stdout and stderr together: return result.stdout + result.stderr. * * There's actually a known issue that "Claude Code's bash tool does not preserve interleaving of stdout and stderr" — they get separated with stdout first, then stderr.

So the API code execution tool keeps them cleanly separated with explicit fields, while Claude Code's Bash tool merges them into a single string.

Code: 384 ()

import subprocess
import sys

@dataclass
class RunResult:
    stdout: str
    stderr: str
    return_code: int

def run(
    absolute_path: str, # The path to a python script
    arguments: List[str] = [], # A list of arguments for the script
) -> RunResult:
    "Runs the python script at the given absolute path"
    
    if not is_valid_absolute_path(absolute_path):
        raise ValueError("The path is not absolute, or the agent is not authorized to read it")
    
    path = Path(absolute_path)

    if path.is_dir():
        raise ValueError("The path does not point to a script")
    
    # I'm comfortable with using this same python, but in production we should probably
    # use some sort of isolation. A virtualenv at the very least.
    result = subprocess.run(
        [sys.executable, str(path), *arguments],
        capture_output=True,
        text=True,
        env={**os.environ, 'PYTHONPATH': str(path.parent)}
    )

    return RunResult(stdout=result.stdout, stderr=result.stderr, return_code=result.returncode)

run("/app/data/skills/skills/pdf/scripts/check_bounding_boxes_test.py")

Output: 82

RunResult(stdout='', stderr='..........\n----------------------------------------------------------------------\nRan 10 tests in 0.001s\n\nOK\n', return_code=0)

Note: 10

Skills demo with claudette

Code: 183 ()

from claudette import *

model = models[2]
display(model)

chat = Chat(model, tools=[skill, read, run])

prompt = "How should I approach announcing in the company newsletter that we are launching a product?"
for o in chat.toolloop(prompt):
    if o.role == 'user':
        pass
    elif hasattr(o, 'stop_reason') and o.stop_reason == 'tool_use':
        for b in o.content:
            if b.type == 'tool_use':
                display(f"Used {b.name} with args {b.input}")
    else:
        display(o)

Output: 4,621

'claude-haiku-4-5'
"Used skill with args {'name': 'internal-comms'}"
"Used read with args {'absolute_path': '/app/data/skills/skills/internal-comms/examples/company-newsletter.md'}"

Perfect! Here's how you should approach announcing a product launch in your company newsletter:

Key Approach

1. Placement & Prominence

  • Feature it in the :megaphone: Company Announcements section at the top
  • This signals it's a major company-wide initiative
  • Keep the announcement concise (1-2 sentences) but impactful

2. What to Include

  • The announcement itself: What the product is, what problem it solves, and why it matters
  • Links to key resources:
    • Product documentation or launch page
    • Slack announcement channel where people can ask questions
    • Any leadership messages about the launch
    • Customer-facing materials or press releases
  • Call to action: What you want employees to do (learn more, share with customers, provide feedback, etc.)

3. Tone & Style

  • Use "we" language ("We're excited to announce..." or "We've launched...")
  • Keep it short and scannable (remember people read newsletters quickly)
  • Focus on company-wide impact, not technical details
  • Highlight why this matters to the whole company, not just one team

4. Supporting Context

  • If relevant, you might add a second bullet in the announcements section with context like:
    • Timeline/availability information
    • Which teams led the effort
    • Customer impact or market significance
    • Next steps for rollout

5. Additional Sections

  • Consider adding a :dart: Progress on Priorities section if the product launch was a major strategic goal
  • Include relevant team achievements that contributed to the launch

Example Structure

Copied!
:megaphone: Company Announcements
- We're thrilled to announce the launch of [Product Name]! 
  [1-2 sentence description of what it does and why it matters]. 
  Check out the [launch page] and join [#product-launch-channel] for questions.
- [Product Name] is now available to [customers/beta users/etc.] 
  with [key features]. See the [documentation] for implementation details.

Would you like help drafting the actual announcement text, or do you have specific details about your product launch you'd like to work with?

  • id: msg_01YVnt7HMg8VYkkSMNNz4zFT
  • content: [{'citations': None, 'text': 'Perfect! Here\'s how you should approach announcing a product launch in your company newsletter:\n\n## Key Approach\n\n**1. Placement & Prominence**\n- Feature it in the **:megaphone: Company Announcements** section at the top\n- This signals it\'s a major company-wide initiative\n- Keep the announcement concise (1-2 sentences) but impactful\n\n**2. What to Include**\n- **The announcement itself**: What the product is, what problem it solves, and why it matters\n- **Links to key resources**: \n - Product documentation or launch page\n - Slack announcement channel where people can ask questions\n - Any leadership messages about the launch\n - Customer-facing materials or press releases\n- **Call to action**: What you want employees to do (learn more, share with customers, provide feedback, etc.)\n\n**3. Tone & Style**\n- Use "we" language ("We\'re excited to announce..." or "We\'ve launched...")\n- Keep it short and scannable (remember people read newsletters quickly)\n- Focus on company-wide impact, not technical details\n- Highlight why this matters to the whole company, not just one team\n\n**4. Supporting Context**\n- If relevant, you might add a second bullet in the announcements section with context like:\n - Timeline/availability information\n - Which teams led the effort\n - Customer impact or market significance\n - Next steps for rollout\n\n**5. Additional Sections**\n- Consider adding a **:dart: Progress on Priorities** section if the product launch was a major strategic goal\n- Include relevant team achievements that contributed to the launch\n\n## Example Structure\n```\n:megaphone: Company Announcements\n- We\'re thrilled to announce the launch of [Product Name]! \n [1-2 sentence description of what it does and why it matters]. \n Check out the [launch page] and join [#product-launch-channel] for questions.\n- [Product Name] is now available to [customers/beta users/etc.] \n with [key features]. See the [documentation] for implementation details.\n```\n\nWould you like help drafting the actual announcement text, or do you have specific details about your product launch you\'d like to work with?', 'type': 'text'}]
  • model: claude-haiku-4-5-20251001
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 4169, 'output_tokens': 504, 'server_tool_use': None, 'service_tier': 'standard'}

Code: 430 ()

from claudette import *
from IPython.display import HTML

model = models[1]
display(model)

def show_art(
    html: str # The art as an HTML string
):
    "Displays a finished artwork to the user"
def show_art(html: str):
    "Displays a finished artwork to the user"

    iframe = f'<iframe srcdoc="{html.replace(chr(34), "&quot;")}" width="820" height="820" style="border:none;"></iframe>'
    display(HTML(iframe))

chat = Chat(model, tools=[skill, read, run, show_art])

prompt = """
Please develop a piece of algorithmic art.

It should be a 5 x 5 grid of spinning flowers, where their speed is a function of their distance to the center.

Do not bother with an algorithmic philosophy, and keep it very simple.

When you are done show it to me using the show_art tool. I'll be
embedding it in a page, so make sure to keep the CSS self-contained.
"""
for o in chat.toolloop(prompt, max_tokens=16384):
    if hasattr(o, 'stop_reason') and o.stop_reason == 'tool_use':
        for b in o.content:
            if b.type == 'tool_use':
                display(f"Used {b.name} with args {b.input}")
    if o.role != 'user':
        display(o)

Output: 8,490

'claude-sonnet-4-5'
"Used skill with args {'name': 'algorithmic-art'}"

I'll create a simple algorithmic art piece with spinning flowers in a 5x5 grid. Let me use the algorithmic-art skill to build this.

  • id: msg_01TU5yPzUjFQJB1afuivczsd
  • content: [{'citations': None, 'text': "I'll create a simple algorithmic art piece with spinning flowers in a 5x5 grid. Let me use the algorithmic-art skill to build this.", 'type': 'text'}, {'id': 'toolu_01BYS99555F9VMJ2JG2ukRnS', 'input': {'name': 'algorithmic-art'}, 'name': 'skill', 'type': 'tool_use'}]
  • model: claude-sonnet-4-5-20250929
  • role: assistant
  • stop_reason: tool_use
  • stop_sequence: None
  • type: message
  • usage: {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 2942, 'output_tokens': 89, 'server_tool_use': None, 'service_tier': 'standard'}
"Used read with args {'absolute_path': '/app/data/skills/skills/algorithmic-art/templates/viewer.html'}"

The user specifically asked me not to bother with an algorithmic philosophy and to keep it very simple. Let me skip the philosophy step and go straight to implementation. First, let me read the template to understand the structure:

  • id: msg_01GbAuUn63TQuTYvbbwb2fbT
  • content: [{'citations': None, 'text': 'The user specifically asked me not to bother with an algorithmic philosophy and to keep it very simple. Let me skip the philosophy step and go straight to implementation. First, let me read the template to understand the structure:', 'type': 'text'}, {'id': 'toolu_01Xdre4RZKUogGpEqbCm63cs', 'input': {'absolute_path': '/app/data/skills/skills/algorithmic-art/templates/viewer.html'}, 'name': 'read', 'type': 'tool_use'}]
  • model: claude-sonnet-4-5-20250929
  • role: assistant
  • stop_reason: tool_use
  • stop_sequence: None
  • type: message
  • usage: {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 7669, 'output_tokens': 117, 'server_tool_use': None, 'service_tier': 'standard'}
'Used show_art with args {\'html\': \'<!DOCTYPE html>\\n<html lang="en">\\n<head>\\n    <meta charset="UTF-8">\\n    <meta name="viewport" content="width=device-width, initial-scale=1.0">\\n    <title>Spinning Flowers Grid</title>\\n    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>\\n    <style>\\n        * {\\n            margin: 0;\\n            padding: 0;\\n            box-sizing: border-box;\\n        }\\n\\n        body {\\n            font-family: -apple-system, BlinkMacSystemFont, \\\'Segoe UI\\\', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\\n            background: #f5f5f5;\\n            display: flex;\\n            justify-content: center;\\n            align-items: center;\\n            min-height: 100vh;\\n            padding: 20px;\\n        }\\n\\n        #canvas-container {\\n            background: white;\\n            border-radius: 8px;\\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\\n            overflow: hidden;\\n        }\\n\\n        #canvas-container canvas {\\n            display: block;\\n        }\\n    </style>\\n</head>\\n<body>\\n    <div id="canvas-container"></div>\\n\\n    <script>\\n        let params = {\\n            seed: 12345,\\n            gridSize: 5,\\n            cellSize: 120,\\n            petalCount: 8,\\n            petalLength: 40,\\n            petalWidth: 15,\\n            centerRadius: 8\\n        };\\n\\n        function setup() {\\n            let canvasSize = params.gridSize * params.cellSize;\\n            let canvas = createCanvas(canvasSize, canvasSize);\\n            canvas.parent(\\\'canvas-container\\\');\\n            \\n            randomSeed(params.seed);\\n            noiseSeed(params.seed);\\n        }\\n\\n        function draw() {\\n            background(250);\\n            \\n            // Calculate center of grid (in grid coordinates)\\n            let centerGridX = (params.gridSize - 1) / 2;\\n            let centerGridY = (params.gridSize - 1) / 2;\\n            \\n            // Draw each flower in the grid\\n            for (let i = 0; i < params.gridSize; i++) {\\n                for (let j = 0; j < params.gridSize; j++) {\\n                    // Calculate position\\n                    let x = i * params.cellSize + params.cellSize / 2;\\n                    let y = j * params.cellSize + params.cellSize / 2;\\n                    \\n                    // Calculate distance from center (in grid units)\\n                    let dx = i - centerGridX;\\n                    let dy = j - centerGridY;\\n                    let distance = sqrt(dx * dx + dy * dy);\\n                    \\n                    // Speed is a function of distance from center\\n                    // Closer to center = slower, farther = faster\\n                    let speed = distance * 0.3 + 0.1;\\n                    \\n                    // Calculate rotation based on time and speed\\n                    let rotation = frameCount * 0.02 * speed;\\n                    \\n                    // Draw the flower\\n                    drawFlower(x, y, rotation, distance);\\n                }\\n            }\\n        }\\n\\n        function drawFlower(x, y, rotation, distance) {\\n            push();\\n            translate(x, y);\\n            rotate(rotation);\\n            \\n            // Color based on distance (hue shift)\\n            let hue = (distance * 30 + frameCount * 0.5) % 360;\\n            colorMode(HSB, 360, 100, 100);\\n            \\n            // Draw petals\\n            for (let i = 0; i < params.petalCount; i++) {\\n                let angle = (TWO_PI / params.petalCount) * i;\\n                push();\\n                rotate(angle);\\n                \\n                // Petal color\\n                fill(hue, 70, 90);\\n                stroke(hue, 80, 70);\\n                strokeWeight(1);\\n                \\n                // Draw petal as an ellipse\\n                ellipse(params.petalLength / 2, 0, params.petalLength, params.petalWidth);\\n                \\n                pop();\\n            }\\n            \\n            // Draw center\\n            fill(hue, 90, 80);\\n            stroke(hue, 100, 60);\\n            strokeWeight(1);\\n            circle(0, 0, params.centerRadius * 2);\\n            \\n            pop();\\n        }\\n    </script>\\n</body>\\n</html>\'}'

Now I'll create a simple spinning flowers grid. Let me build this directly without the philosophy since the user requested simplicity:

  • id: msg_01VRbzbQTKux1Pepa4pricTr
  • content: [{'citations': None, 'text': "Now I'll create a simple spinning flowers grid. Let me build this directly without the philosophy since the user requested simplicity:", 'type': 'text'}, {'id': 'toolu_013RD1aZjDX5pskFsg6pc7bw', 'input': {'html': '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Spinning Flowers Grid</title>\n <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n background: #f5f5f5;\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n padding: 20px;\n }\n\n #canvas-container {\n background: white;\n border-radius: 8px;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n }\n\n #canvas-container canvas {\n display: block;\n }\n </style>\n</head>\n<body>\n <div id="canvas-container"></div>\n\n <script>\n let params = {\n seed: 12345,\n gridSize: 5,\n cellSize: 120,\n petalCount: 8,\n petalLength: 40,\n petalWidth: 15,\n centerRadius: 8\n };\n\n function setup() {\n let canvasSize = params.gridSize * params.cellSize;\n let canvas = createCanvas(canvasSize, canvasSize);\n canvas.parent(\'canvas-container\');\n \n randomSeed(params.seed);\n noiseSeed(params.seed);\n }\n\n function draw() {\n background(250);\n \n // Calculate center of grid (in grid coordinates)\n let centerGridX = (params.gridSize - 1) / 2;\n let centerGridY = (params.gridSize - 1) / 2;\n \n // Draw each flower in the grid\n for (let i = 0; i < params.gridSize; i++) {\n for (let j = 0; j < params.gridSize; j++) {\n // Calculate position\n let x = i * params.cellSize + params.cellSize / 2;\n let y = j * params.cellSize + params.cellSize / 2;\n \n // Calculate distance from center (in grid units)\n let dx = i - centerGridX;\n let dy = j - centerGridY;\n let distance = sqrt(dx * dx + dy * dy);\n \n // Speed is a function of distance from center\n // Closer to center = slower, farther = faster\n let speed = distance * 0.3 + 0.1;\n \n // Calculate rotation based on time and speed\n let rotation = frameCount * 0.02 * speed;\n \n // Draw the flower\n drawFlower(x, y, rotation, distance);\n }\n }\n }\n\n function drawFlower(x, y, rotation, distance) {\n push();\n translate(x, y);\n rotate(rotation);\n \n // Color based on distance (hue shift)\n let hue = (distance * 30 + frameCount * 0.5) % 360;\n colorMode(HSB, 360, 100, 100);\n \n // Draw petals\n for (let i = 0; i < params.petalCount; i++) {\n let angle = (TWO_PI / params.petalCount) * i;\n push();\n rotate(angle);\n \n // Petal color\n fill(hue, 70, 90);\n stroke(hue, 80, 70);\n strokeWeight(1);\n \n // Draw petal as an ellipse\n ellipse(params.petalLength / 2, 0, params.petalLength, params.petalWidth);\n \n pop();\n }\n \n // Draw center\n fill(hue, 90, 80);\n stroke(hue, 100, 60);\n strokeWeight(1);\n circle(0, 0, params.centerRadius * 2);\n \n pop();\n }\n </script>\n</body>\n</html>'}, 'name': 'show_art', 'type': 'tool_use'}]
  • model: claude-sonnet-4-5-20250929
  • role: assistant
  • stop_reason: tool_use
  • stop_sequence: None
  • type: message
  • usage: {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 13099, 'output_tokens': 1224, 'server_tool_use': None, 'service_tier': 'standard'}

Perfect! I've created a simple algorithmic art piece featuring a 5×5 grid of spinning flowers. Here's what I implemented:

Features:

  • 5×5 Grid: 25 flowers arranged in a perfect grid
  • Speed based on distance: Flowers closer to the center spin slower, while those farther away spin faster
  • Color variation: Each flower's color shifts based on its distance from center and time, creating a dynamic rainbow effect
  • Simple flower design: Each flower has 8 petals arranged radially around a center circle
  • Smooth animation: Continuous rotation creates a mesmerizing effect

The CSS is completely self-contained within the HTML file, so you can easily embed it in any page. The flowers use a simple ellipse-based petal design and HSB color mode for smooth color transitions.

  • id: msg_01HZLnFMGuNDhnAU7grRAT5e
  • content: [{'citations': None, 'text': "Perfect! I've created a simple algorithmic art piece featuring a 5×5 grid of spinning flowers. Here's what I implemented:\n\n**Features:**\n- **5×5 Grid**: 25 flowers arranged in a perfect grid\n- **Speed based on distance**: Flowers closer to the center spin slower, while those farther away spin faster\n- **Color variation**: Each flower's color shifts based on its distance from center and time, creating a dynamic rainbow effect\n- **Simple flower design**: Each flower has 8 petals arranged radially around a center circle\n- **Smooth animation**: Continuous rotation creates a mesmerizing effect\n\nThe CSS is completely self-contained within the HTML file, so you can easily embed it in any page. The flowers use a simple ellipse-based petal design and HSB color mode for smooth color transitions.", 'type': 'text'}]
  • model: claude-sonnet-4-5-20250929
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 14336, 'output_tokens': 186, 'server_tool_use': None, 'service_tier': 'standard'}

Note: 4

Final implementation

Prompt: 21

Please consolidate the tools, helper functions and import into a single python cell

Assistant: 496

Copied!
{
  "id": "toolu_01QYmJ8ibZiinFt6UjeWcBtQ",
  "call": {
    "function": "add_msg",
    "arguments": {
      "msg_type": "code",
      "content": "import os\nimport sys\nimport subprocess\nimport frontmatter\nimport xml.etree.ElementTree as ET\nfrom pathlib import Path\nfrom dataclasses import dataclass\nfrom typing import List\n\n# --- Data classes ---\n\n@dataclass\nclass Skill:\n    name: str\n    description: str\n    location: str = None\n\n    @classmethod\n    def from_directory(cls, directory):\n        location = os.path.join(directory, 'SKILL.md')\n        post = frontmatter.load(location)\n        return cls(name=post['name'], description=post['description'], location=location)\n\n    def instructions(self):\n        with open(self.location) as f:\n  <TRUNCATED>"
    }
  },
  "result": "'_541fcb44'"
}

Done! I've consolidated everything into a single cell with:

  • Imports at the top
  • Data classes: Skill and RunResult
  • Helpers: load_skills(), available_skills(), is_valid_absolute_path()
  • Tools: skill(), read(), run()

Run the cell to make sure it executes cleanly?

Code: 1,369 ()

import os
import sys
import subprocess
import frontmatter
import xml.etree.ElementTree as ET
from pathlib import Path
from dataclasses import dataclass
from typing import List

# --- Data classes ---

@dataclass
class Skill:
    name: str
    description: str
    location: str = None

    @classmethod
    def from_directory(cls, directory):
        location = os.path.join(directory, 'SKILL.md')
        post = frontmatter.load(location)
        return cls(name=post['name'], description=post['description'], location=location)

    def instructions(self):
        with open(self.location) as f:
            return f.read()

@dataclass
class RunResult:
    stdout: str
    stderr: str
    return_code: int

# --- Helper functions ---

def load_skills() -> List[Skill]:
    return [
        Skill.from_directory(full_path)
        for skills_root in os.environ.get('SKILLS_PATH', '').split(":")
        for directory in os.listdir(skills_root)
        if os.path.isdir(full_path := os.path.join(skills_root, directory))
    ]

def available_skills() -> str:
    """Returns a skill description block to be added to the system prompt"""
    skills = load_skills()
    root = ET.Element('available_skills')
    for s in skills:
        skill_el = ET.SubElement(root, 'skill')
        ET.SubElement(skill_el, 'name').text = s.name
        ET.SubElement(skill_el, 'description').text = s.description
        ET.SubElement(skill_el, 'location').text = s.location
    ET.indent(root)
    return ET.tostring(root, encoding='unicode')

def is_valid_absolute_path(path: str) -> bool:
    path = Path(path)
    if not path.is_absolute():
        return False
    valid_roots = [Path(root) for root in os.environ.get('SKILLS_PATH', '').split(":")]
    return any(path.is_relative_to(root) for root in valid_roots)

# --- Tools ---

def skill(
    name: str,  # The name of a skill to activate
) -> str:  # The contents of the skill's SKILL.MD
    for skills_root in os.environ.get('SKILLS_PATH', '').split(":"):
        skill_path = os.path.join(skills_root, name)
        if os.path.exists(skill_path):
            return Skill.from_directory(skill_path).instructions()

skill.__doc__ = f"""
Execute a skill within the main conversation

When users ask you to perform tasks, check if any of the available skills below
can help complete the task more effectively. Skills provide specialized
capabilities and domain knowledge.

How to use skills:
- Invoke skill using this tool with the skill name only (no arguments).
- When you invoke a skill, the skill's prompt will expand and provide
    detailed instructions on how to complete the task.

Important:
- Only use skills listed in <available_skills> below.
- Do not invoke a skill that is already running.

{available_skills()}
"""

def read(
    absolute_path: str  # Absolute path of the file to read
) -> str:  # Text contents of the read file
    """Returns the text contents of the file at the given absolute path"""
    if not is_valid_absolute_path(absolute_path):
        raise ValueError("The path is not absolute, or the agent is not authorized to read it")
    with open(absolute_path, 'r') as f:
        return f.read()

def run(
    absolute_path: str,  # The path to a python script
    arguments: List[str] = [],  # A list of arguments for the script
) -> RunResult:
    """Runs the python script at the given absolute path"""
    if not is_valid_absolute_path(absolute_path):
        raise ValueError("The path is not absolute, or the agent is not authorized to read it")
    
    path = Path(absolute_path)
    if path.is_dir():
        raise ValueError("The path does not point to a script")
    
    result = subprocess.run(
        [sys.executable, str(path), *arguments],
        capture_output=True,
        text=True,
        env={**os.environ, 'PYTHONPATH': str(path.parent)}
    )
    return RunResult(stdout=result.stdout, stderr=result.stderr, return_code=result.returncode)