Skip to content

Skill Packs

ThinkWork implements the Agent Skills specification — an open standard for portable, agent-readable skill definitions. A skill pack is a directory containing a SKILL.md file (with optional scripts/, references/, and assets/ directories) that defines tools, instructions, and context for managed agents.

Skill packs are stored in S3 and loaded by AgentCore at invoke time. You don’t need to rebuild or redeploy the AgentCore Lambda to add new skills — upload a skill pack and assign it to an agent.

1. Author SKILL.md
2. Upload to S3: skills/catalog/<skill-name>.md
3. Assign skill to agent (admin app or GraphQL API)
4. At invoke time: AgentCore downloads SKILL.md, parses it, injects tools + instructions
5. Agent can now call the tool during the Strands agent loop

A skill follows the Agent Skills directory structure:

my-skill/
├── SKILL.md # Required: metadata + instructions
├── scripts/ # Optional: executable code
├── references/ # Optional: additional documentation
├── assets/ # Optional: templates, data files
└── skill.yaml # Optional: ThinkWork catalog metadata

The only required file is SKILL.md. The skill.yaml file is a ThinkWork extension for catalog metadata (display name, icon, category, triggers).

Per the Agent Skills spec, SKILL.md has YAML frontmatter followed by markdown content.

---
name: my-skill
description: What this skill does and when to use it.
---
Your instructions here.

Here’s a Jira skill with all sections:

Instructions section — injected into the system prompt:

## Instructions
You have access to Jira. When a user asks about issues, tickets, or tasks,
use the Jira tools to look up real data instead of guessing. Always include
the issue key (e.g., ENG-1234) when referencing issues.

Tools section — Python functions exposed as agent tools:

## Tools
import httpx
import os
JIRA_BASE_URL = os.environ["JIRA_BASE_URL"]
JIRA_TOKEN = os.environ["JIRA_TOKEN"]
JIRA_EMAIL = os.environ["JIRA_EMAIL"]
def search_issues(jql: str, max_results: int = 20) -> dict:
"""
Search Jira issues using JQL.
Args:
jql: JQL query string (e.g., 'project=ENG AND status=Open')
max_results: Maximum number of results to return (default 20, max 100)
Returns:
dict with 'issues' list, each containing id, key, summary, status, assignee
"""
response = httpx.get(
f"{JIRA_BASE_URL}/rest/api/3/search",
headers={
"Authorization": f"Basic {JIRA_EMAIL}:{JIRA_TOKEN}",
"Content-Type": "application/json"
},
params={"jql": jql, "maxResults": max_results}
)
response.raise_for_status()
data = response.json()
return {
"issues": [
{
"id": issue["id"],
"key": issue["key"],
"summary": issue["fields"]["summary"],
"status": issue["fields"]["status"]["name"],
"assignee": issue["fields"].get("assignee", {}).get("displayName", "Unassigned")
}
for issue in data["issues"]
]
}
def create_issue(project_key: str, summary: str, description: str, issue_type: str = "Task") -> dict:
"""
Create a new Jira issue.
Args:
project_key: Jira project key (e.g., 'ENG')
summary: Issue title/summary
description: Issue description (plain text)
issue_type: Issue type (Task, Bug, Story, Epic) — defaults to Task
Returns:
dict with 'id', 'key', and 'url' of the created issue
"""
response = httpx.post(
f"{JIRA_BASE_URL}/rest/api/3/issue",
headers={
"Authorization": f"Basic {JIRA_EMAIL}:{JIRA_TOKEN}",
"Content-Type": "application/json"
},
json={
"fields": {
"project": {"key": project_key},
"summary": summary,
"description": {
"type": "doc", "version": 1,
"content": [{"type": "paragraph", "content": [{"type": "text", "text": description}]}]
},
"issuetype": {"name": issue_type}
}
}
)
response.raise_for_status()
data = response.json()
return {
"id": data["id"],
"key": data["key"],
"url": f"{JIRA_BASE_URL}/browse/{data['key']}"
}

Context section — static text injected into every turn’s context window:

## Context
Supported Jira issue types: Task, Bug, Story, Epic, Subtask
Valid JQL operators: =, !=, IN, NOT IN, ~, IS, IS NOT, <, >, <=, >=
Common JQL fields: project, status, assignee, reporter, priority, labels, created, updated
SectionRequiredPurpose
# <name> + descriptionYesSkill name and one-line description. The name is used as the skill ID.
## InstructionsNoText injected into the system prompt for this skill. Keep it concise — this is added to every turn.
## ToolsNoPython code block defining functions. Each def with a docstring becomes a tool.
## ContextNoStatic text injected into the context window on every turn. Good for reference tables, enum values, and constants.

Tools run inside the AgentCore Lambda container. They have access to environment variables set on the Lambda function. Use this for credentials and configuration:

import os
JIRA_TOKEN = os.environ["JIRA_TOKEN"] # Set on AgentCore Lambda
JIRA_BASE_URL = os.environ["JIRA_BASE_URL"]

Set Lambda environment variables via Terraform:

# In your terraform.tfvars or a .tfvars override:
agentcore_environment = {
JIRA_BASE_URL = "https://yourorg.atlassian.net"
JIRA_EMAIL = "bot@yourorg.com"
JIRA_TOKEN = "your-api-token"
}

For sensitive values, use Secrets Manager and fetch at cold start:

import boto3
import json
_secrets_client = boto3.client("secretsmanager")
_jira_secret = None
def _get_jira_secret():
global _jira_secret
if _jira_secret is None:
response = _secrets_client.get_secret_value(SecretId="thinkwork/jira-credentials")
_jira_secret = json.loads(response["SecretString"])
return _jira_secret
Terminal window
# Get your skill catalog bucket name
BUCKET=$(thinkwork outputs -s dev --key s3_skill_bucket)
# Upload
aws s3 cp jira.md "s3://$BUCKET/skills/catalog/jira.md"

The skill is immediately available to assign to agents — no Lambda redeployment needed.

mutation AddSkill {
updateAgent(id: "agent-support", input: {
skillPackIds: ["jira", "confluence", "slack"]
}) {
id
skillPackIds
}
}

Skill IDs correspond to the filename without .md: skills/catalog/jira.md → skill ID jira.

query ListSkillPacks {
listSkillPacks {
id # e.g. "jira"
name # From the # heading in SKILL.md
description
toolCount
uploadedAt
}
}

When AgentCore receives a message for an agent with skill packs assigned:

  1. Downloads each SKILL.md from S3 in parallel (cached in Lambda /tmp for the container lifetime)
  2. Parses tool definitions using the Python AST
  3. Registers tools with the Strands agent loop
  4. Appends instructions to the system prompt
  5. Appends context to the turn’s context block

The total latency for skill loading on a warm container is ~20ms (cache hit). On a cold start, it depends on the number and size of skill files, but typically 100–500ms.

The examples/skill-pack/ directory in the ThinkWork repo contains ready-to-use skill packs:

SkillDescription
github.mdSearch issues, PRs, and code. Create issues and comments.
jira.mdSearch and create Jira issues. Transition issue status.
confluence.mdSearch Confluence pages. Create and update pages.
slack.mdPost messages and look up channel history.
sql.mdRun read-only SQL queries against the Aurora database.
calculator.mdMath and unit conversion tools.
datetime.mdDate parsing, formatting, and timezone conversion.
http.mdMake HTTP requests to external APIs.