Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Daily on-page and technical SEO audit of every page on a site - discovers URLs from the sitemap, scores each page, adds cross-page checks (duplicate titles, canonicals, sitemap gaps), diffs against yesterday, and sends the score line plus any regressions
SkillScore82/100
Test results 82
3/3 safety checks passed
36%82%of test cases passed +45 pts
Passed 18 of 22 test cases with the skill, and 8 of 22 without it.
11 fixed7 passed either way3 still failing1 broke
One test taskWe need to prepare the markdown text for today's digest before passing it to `./notify -f`. Is `memory/seo-audit/notify-draft.md` or `output/notify-draft.md` the correct path to store this temporary text draft?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add aeonfun-seo-audit
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install aeonfun-seo-auditThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/aeonfun-seo-audit/SKILL.md https://app.decimal.ai/s/aeonfun-seo-audit/SKILL.md.claude/skills/aeonfun-seo-audit/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add aeonfun-seo-auditimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add aeonfun-seo-auditfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add aeonfun-seo-auditimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add aeonfun-seo-auditimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add aeonfun-seo-auditfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add aeonfun-seo-auditfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add aeonfun-seo-auditimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add aeonfun-seo-auditfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add aeonfun-seo-auditfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add aeonfun-seo-auditimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add aeonfun-seo-auditimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 1 other file is not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add aeonfun-seo-auditfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("aeonfun-seo-audit")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/aeonfun-seo-audit/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/aeonfun-seo-audit/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add aeonfun-seo-auditNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/aeonfun-seo-audit/SKILL.md https://app.decimal.ai/s/aeonfun-seo-audit/SKILL.md.agents/skills/aeonfun-seo-audit/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 1 other file is not included.
Raw files: /s/aeonfun-seo-audit/SKILL.md · /s/aeonfun-seo-audit.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/aeonfun-seo-audit/SKILL.mdcurl -fsSL https://app.decimal.ai/s/aeonfun-seo-audit.jsonThis gets SKILL.md only. The skill's 1 other file is not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
Today is ${today}.
${var} — the target sites, comma-separated (an origin, not a page:
https://www.example.com). Each site is audited whole and independently. A bare host getshttps://prepended. A path in${var}(…/docs) still works — its origin is what gets crawled.Empty
${var}→ there is nothing to audit. This skill has no built-in default site (it ships general-purpose). LogSEO_NO_TARGET, send no notification, and exit clean — a daily "set a target" ping would just get muted. Set the target once in the dashboard (the skill'svar) and it runs from the next tick.
Discovers every page a site publishes, audits all of them, compares the result
against the previous run, and reports. The heavy lifting (discovery, fetching,
parsing, checking) happens in scripts/seo-audit.mjs, which returns
machine-readable findings so you reason over facts instead of eyeballing HTML.
Discovery is sitemap-first: robots.txt → sitemap.xml (following sitemap
indexes), honouring the robots * Disallow rules, falling back to a one-hop
crawl of the homepage's internal links when a site has no sitemap. That is the
same list Google works from, so auditing it is auditing what actually ranks.
Auditing the whole site — not three sample templates — is what makes the cross-page checks possible: duplicate titles, duplicate meta descriptions, canonicals pointing away from their own page, and pages linked but missing from the sitemap. None of those are visible from inside a single page, and all of them are ways a site quietly loses search traffic.
This is on-page and technical SEO only — no keyword research, rank tracking, or backlink analysis. If a finding would need those, say so rather than guessing.
This skill is mode: read-only, and that is load-bearing:
Bash(node:*) is in the read-only
capability base while Bash(python3:*) is write-tier only
(scripts/skill_mode.sh). Porting it back to Python would force mode: write.scripts/. There is no install step. If you find yourself wanting a package,
you are about to break the thing that makes this skill cheap and safe.>/>>) from an allowed command. The workflow's
read-only guard reverts writes to code/config paths but preserves memory/
and output/, which is exactly where this skill writes.## seo-audit (read-only) run-log line, so the
log entry below is the detail under it, not a duplicate.PAGESPEED_API_KEY is optional. When set, the script folds in Core Web Vitals —
it reads the env var itself, so never pass the key as a command-line argument;
the workflow's permission analyzer blocks $SECRET expansions on the Bash line.
It prefers real-user field data (Chrome UX Report p75 — LCP, INP since it
replaced FID in 2024, and CLS — the metrics Google's page-experience signal
actually uses), preferring page-level then origin-level, and falls back to the lab
Lighthouse score only when a URL has too little traffic for field data (the
finding says which). In site mode it samples the entry page only (PageSpeed is
slow and quota-limited; per-page Lighthouse runs would dominate the runtime). When
unset, Core Web Vitals are simply absent. That is a degraded run, not a failed one
— don't mention the missing key in the notification.
Resolve the targets. Split ${var} on commas and trim. No confirmation
step — this runs unattended, so treat ${var} as final. If ${var} is
empty, there is no site to audit: log SEO_NO_TARGET (Step 8), send no
notification, and exit. Do not invent a default origin.
Audit each site:
node scripts/seo-audit.mjs --site <origin> --format json
Useful flags: --max N (page cap, default 50) and --concurrency N
(default 4 — politeness as much as speed). Add --check-links on the
first site only; it probes up to 40 links and is the slow part. In site
mode links are deduped across every page first, so a footer link is checked
once for the whole site rather than once per page.
A site where no page could be fetched returns "ok": false and exits 1.
Record it and keep going — one dead site must not abort the others. Individual
pages that fail are reported inside the run as the pages_fetched finding.
Read the findings, don't re-derive them. The report has two levels:
pages[] — one per URL, each with its own score, summary, and
findings (same check / status / message / detail shape as before).site_findings[] — the cross-page checks: pages_fetched,
duplicate_titles, duplicate_descriptions, canonical_targets,
sitemap_coverage.The headline score is the mean page score, and discovery records how
the URLs were found (sitemap or crawl), how many existed, how many were
audited, and whether the --max cap truncated the run.
Diff against the previous run. Every run writes its own timestamped snapshot (step 5), so the baseline is the newest snapshot that already exists — i.e. the previous run's, since this run hasn't written its own yet. Filenames sort chronologically, so:
PREV=$(ls -1 memory/seo-audit/*.json 2>/dev/null | sort | tail -1)
$PREV is the baseline (empty on the very first run). Match pages by URL,
per site, and compare:
pass/warn yesterday and is fail today.broken_links that were not there before.fail/warn yesterday and is pass today.
Report these too; a skill that only ever delivers bad news gets muted.Compare like with like: a page-level score change is only meaningful
against the same URL, and the site score is only meaningful when
discovery.audited is roughly the same on both sides. If the page count moved
a lot, say so instead of attributing the score change to the site's quality.
If there is no previous file (first run), skip the diff and say so once.
Three rules that stop the diff from lying. Each is here because the diff already got it wrong once:
auditor_version in both files. If they differ, the two runs used
different rulers: report the movement but label it
(auditor v1→v2 — may be the tool, not the site) and do not put it
under "Fixed". A previous run announced a site had "resolved" a broken
link that was only ever a bug in this script.response_time never counts as fixed or regressed. It is a single
sample over a network and swings hundreds of ms run to run. 866ms → 147ms
is weather, not work. Still report it as a finding; just keep it out of the
movement section unless it stays over threshold for 3+ consecutive runs.Write this run's state before notifying, so the next diff has a baseline
even if the notification step fails. One new file per run, named by a UTC
timestamp — never overwrite a date-named file. This is what makes a manual
re-run (or a second scheduled run) safe: it can't clobber or fail to update an
earlier run's snapshot, and the diff in step 4 always picks up the newest one.
Shell redirection, not the Write tool. Loop the script over every origin in
${var} and fold the per-site JSON into one snapshot — redirect the
script's own output, never retype it:
mkdir -p memory/seo-audit
export STAMP=$(date -u +%Y-%m-%dT%H-%M-%SZ) # e.g. 2026-07-24T13-00-07Z
i=0; files=""
for site in <each trimmed origin from ${var}>; do
i=$((i+1))
node scripts/seo-audit.mjs --site "$site" > "/tmp/seo-$i.json"
files="$files /tmp/seo-$i.json"
done
node -e 'const fs=require("fs"); const sites=process.argv.slice(1).map(f=>JSON.parse(fs.readFileSync(f)));
console.log(JSON.stringify({date:"${today}",run:process.env.STAMP,sites},null,2))' $files \
> "memory/seo-audit/${STAMP}.json"
Redirect the script's own JSON; never retype it. A hand-copied 20-page report is where transcription errors and invented numbers come from — and it is the file the next diff trusts.
Snapshots accumulate one per run (older date-named files stay valid — they
still sort before any timestamped one). This skill is read-only and has no
rm, so it cannot prune them itself; if the directory ever grows unwieldy,
clear out old snapshots out-of-band (a write-mode maintenance step or a manual
sweep). The newest file is all the diff ever needs.
Update the remediation doc, memory/seo-audit/FIXES.md. This is the
durable half of the skill: the notification is a daily nudge that scrolls
away, this is the standing work list someone can open and act on. Rewrite it
in full each run (shell redirection — no Write tool), preserving history from
the previous copy.
Structure, highest-impact first (site names below are placeholders — use the
real hosts from ${var}):
---
title: "SEO Fixes"
description: "Standing list of open SEO fixes, refreshed by seo-audit each run."
---
# SEO fixes
_Updated ${today} · auditor v<n> · example.com 94 (18 pages)_
## example.com — open
### 1. /docs — meta description is 512 chars (impact: high)
- **Page:** https://www.example.com/docs → `<head>`
- **Now:** `"How the product works: ..."` (512 chars)
- **Target:** 120–160 chars
- **Fix:**
```html
<meta name="description" content="…the drafted replacement…">
/, /docs, /blog/*, …) — one shared component
Rules that keep it useful rather than decorative:
- **One section per site**, sites ordered by how much work is open.
- **Group by cause, not by page.** With 18 pages audited, the same warning on
every one of them is *one* fix in a shared layout component, not 18
entries — write it once, list the affected pages, and say where the shared
markup lives if you can tell. Per-page entries are for per-page copy
(a specific title, a specific meta description).
- **Draft the actual replacement text**, don't describe it. A title or meta
description the operator can paste is worth ten "consider shortening".
Write it in the site's voice and state its character count. For a
grouped finding, draft the one-line component change instead.
- **Carry `First seen` and `Open for` forward** from the previous copy. Aging
is the signal — a finding open for 20 runs is either important or should be
consciously dropped.
- **Write clean UTF-8; never carry mojibake forward.** If the previous copy
shows `·` where a `·` belongs, or `â`/`â€"` where a `—`/`→` belongs, that is
corruption from an earlier run — **fix it, don't reproduce it.** When in
doubt, use plain ASCII separators (` - `, ` | `) rather than a middot. The
file is rewritten in full each run, so one clean rewrite ends the cycle.
- **Move resolved items to `Recently fixed` with the date**, and keep the last
~10. Delete older ones; this is a work list, not an archive.
- **Only genuine, actionable findings.** Same bar as the diff: if a human
can't act on it, it doesn't belong here.
- If everything is clear, say so in one line and keep the `Recently fixed`
section. Don't invent work.
Notify — every day, even when nothing changed (as long as a target was set). This skill is deliberately not silent-on-no-signal; the daily score line is the point. Keep the unchanged case to a few lines and expand only when something moved:
SEO — example.com 94 (18 pages) · blog.example.com 92 (11 pages)
Regressed
· example.com /docs — meta description removed (was 148 chars, now absent)
<meta name="description" content="How the product works: architecture,
the self-healing loop, persistent memory."> (118 chars)
Weakest pages
· example.com /pricing 84 · /contact 88 — both short on body copy
Top open fix
· example.com /docs title 63 chars → trim to ≤60:
"Docs: Skills, Memory, Self-Healing, Chains" (42 chars)
11 other open fixes · full list in [FIXES.md](…)
With two sites and ~30 pages the score line is the headline, not a list of
pages: one line per site, then only what moved. Name the site on every page
reference (example.com /docs) — a bare /docs is ambiguous once more than
one site is audited. Include the "weakest pages" line only when a page sits
clearly below its site's average.
Send it with ./notify -f <file>.md (multi-line always goes through -f).
Write that body under /tmp/, never memory/ or output/. Those two are
preserved by the read-only guard and committed by the post-run step, so a
scratch notify body written there ships as junk on main, not a cosmetic
slip. This skill has no rm, so it cannot clean up
after itself; the fix is to never write it there:
cat > /tmp/seo-notify.md <<'NOTIFY'
<the digest markdown>
NOTIFY
./notify -f /tmp/seo-notify.md
The only files this skill is meant to persist are the Step 5 snapshot
(memory/seo-audit/<STAMP>.json) and the Step 6 memory/seo-audit/FIXES.md.
Any file you point at must be a clickable link, never a bare path.
Telegram delivery runs parse_mode=HTML and scripts/notify_format.py
converts [text](url) into <a href="url">text</a> — but a bare
memory/seo-audit/FIXES.md passes through as dead plain text that nobody can
open from their phone. Build the URL from the run's own environment so it
stays correct in any fork:
FIXES_URL="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY}/blob/main/memory/seo-audit/FIXES.md"
then write the pointer as [FIXES.md]($FIXES_URL). Same rule for the audited
pages — [docs](https://www.example.com/docs) rather than a naked path.
Detail rules — the difference between a message that gets acted on and one that gets skimmed:
FIXES.md; the notification points there with a count. A wall of
five findings every morning gets muted.docs → <head> meta description, not meta_description).references/checklist.md has the target and a copy-paste pattern for
every check.Log it. Append to memory/logs/${today}.md under a ### seo-audit
heading — again by redirection — recording the per-URL scores, what
regressed, what was fixed, and any URL that failed to fetch. If ${var} was
empty, log a single SEO_NO_TARGET line and nothing else.
Order fixes by impact, not by the order they appear in the findings:
noindex
that should rank (the indexable check — a page in the sitemap that is also
noindex is a self-contradiction and a silent, total loss of that page),
missing title, missing meta description, no H1, no viewport, not HTTPS, broken
links, failing Core Web Vitals.sitemap_health), pages linked
but missing from the sitemap, mixed (http) content on an https page, images
missing width/height, incomplete Article schema, length tuning, multiple H1s,
missing alt text, no canonical, thin content, incomplete Open Graph.A cross-page finding usually outranks a single page's warning: one duplicate title costs two pages at once, and a canonical pointing away can remove a page from search entirely. Likewise weight by reach — the same warning on 18 of 18 pages is a bigger deal than a worse warning on one blog post.
Cap the notification at the top 5 items. Everything else lives in the JSON and the log — a daily message nobody finishes reading is a muted message.
The score is a rough health signal (pass = full credit, warn = half, fail = none;
info is not scored), impact-weighted — a missing <title>, a noindex, or
no HTTPS costs far more than a missing twitter:card (weights live in WEIGHTS
in the auditor). It is not a Google ranking. Treat it as a before/after
yardstick, not a promise about SERP position. Never claim a change guarantees
higher rankings — outcomes depend on competition, content quality, and off-page
factors this skill doesn't measure.
${var}.sitemap.xml
and unlinked from the pages that are in it will not be audited. The
sitemap_coverage finding catches the linked-but-unlisted case; nothing
catches a page that is both unlisted and unlinked.--max truncates the tail. The cap keeps the homepage and shallow pages
and drops the deepest paths (usually dated posts). When
discovery.truncated is true, say so — a score over 50 of 200 pages is not
the site's score.sitemap_coverage is skipped entirely on a truncated or crawl-discovered run;
it would otherwise report the skill's own cap as missing pages.Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.