Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when migrating, porting, or rewriting frontend code from operate/client/ or tasklist/client/ to the orchestration cluster webapp. Trigger when migrating legacy pages/components, converting React Router to TanStack Router, replacing MobX with TanStack Query, rewriting styled-components as SCSS, converting legacy tests to Vitest browser mode, or asking how a legacy pattern maps to the new app.
SkillScore98/100
Test results 96 · Live runs 100
3/3 safety checks passed
29%96%of test cases passed +67 pts
Passed 23 of 24 test cases with the skill, and 7 of 24 without it.
16 fixed7 passed either way1 still failing0 broke
One test taskAfter migrating several frontend files in `webapp/client/`, what script command should be used to format all modified files?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add camunda-frontend-migrator
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("camunda-frontend-migrator")
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 camunda-frontend-migratorThe 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/camunda-frontend-migrator/SKILL.md https://app.decimal.ai/s/camunda-frontend-migrator/SKILL.md.claude/skills/camunda-frontend-migrator/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 camunda-frontend-migratorimport 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorfrom 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorimport 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorimport 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorfrom 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorfrom 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorimport 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/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 camunda-frontend-migratorfrom 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorfrom 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorimport 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorimport 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorfrom 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("camunda-frontend-migrator")
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/camunda-frontend-migrator/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/camunda-frontend-migrator/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 camunda-frontend-migratorNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/camunda-frontend-migrator/SKILL.md https://app.decimal.ai/s/camunda-frontend-migrator/SKILL.md.agents/skills/camunda-frontend-migrator/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/camunda-frontend-migrator/SKILL.md · /s/camunda-frontend-migrator.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/camunda-frontend-migrator/SKILL.mdcurl -fsSL https://app.decimal.ai/s/camunda-frontend-migrator.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.
The orchestration cluster webapp (webapp/client/apps/orchestration-cluster-webapp/) contains
Tasklist and is replacing the legacy Operate and Admin frontends. Migration is a rewrite using new
patterns, not a code port. Legacy code is the specification of what a feature does; the new code
follows a different architecture for how.
This skill is the translation layer. It maps legacy patterns to their target equivalents so you produce code that fits the new codebase from the start. For target conventions (pod areas + shared structure, data loading tiers, forms, feature flags), defer to the frontend-feature skill. For test-writing details, defer to frontend-unit-test and frontend-integration-test.
Read the legacy code and extract what it does for users, not how it's implemented. Identify:
Focus on behavior. The implementation details (MobX stores, styled-components, React Router hooks) are all going to change.
Look up every endpoint the legacy feature calls in @camunda/camunda-api-zod-schemas. The legacy code typically imports from this package already — check its import statements. If an endpoint is missing from the schema package, it needs to be added before or alongside the migration.
Decide how the feature decomposes in the new architecture. The general rule:
src/routes/_auth/<pod>/src/operate/, src/tasklist/, or src/admin/). The pod decides its own internal structure — there is no prescribed layout.src/shared/<concern>/ — http, auth, config, errors, i18n, theme. Use what's already there; extend it rather than duplicating in the pod area.Produce the code directly. If the decomposition is genuinely ambiguous (e.g., a legacy page that mixes two concerns and could become one page or two), flag it for the user with your recommendation and reasoning.
Dependencies flow one way: routes → pod area → shared. Pod code must never import from a route file. Shared code must never import from a pod area. If a type (like search param schemas) needs to be shared between a route and pod code, define it in the pod area and import it into the route — not the other way around.
This is the core of the migration. Each legacy pattern has a target equivalent — apply them systematically. The summary is below; see references/pattern-mapping.md for side-by-side code examples.
Rewrite tests using the new patterns (Vitest browser mode, MSW via worker fixture, expect.element()). Don't port legacy test code — the testing infrastructure is fundamentally different. See the testing transformation table below and references/pattern-mapping.md for full before/after examples.
# From webapp/client/
npm run prettier:format # Auto-format (always use this script, never npx prettier)
npm run lint
# From webapp/client/apps/orchestration-cluster-webapp/
npm run typecheck # TypeScript across all tsconfigs (always use this script, never npx tsc)
npm run test:unit
| Legacy | Target |
|---|---|
createBrowserRouter + createRoutesFromElements | TanStack Router file-based routes |
lazy={() => import('./Page')} with Component export | createFileRoute in a file under src/routes/ |
Paths.processes() path builder object | File path is the URL — src/routes/_auth/operate/processes/index.tsx |
useParams() / useSearchParams() | useParams({ from: '/route-path' }) / useSearch({ from: '/route-path' }) (typed, validated) |
useNavigate() + navigate(path) | useNavigate() from @tanstack/react-router or <Link> |
useOutletContext() for parent data | Shared queryOptions — child re-calls the same query (TanStack Query deduplicates) |
<Navigate to={path} /> redirect | throw redirect({ to: path }) in beforeLoad |
ErrorBoundary per route | errorComponent on the route definition |
Auth-gated routes go under _auth/ (pathless layout route). Product routes go under _auth/operate/, _auth/tasklist/, or _auth/admin/.
| Legacy | Target |
|---|---|
use*.query.ts files scattered across modules | Centralized queryOptions in #/shared/http/queries.ts |
| Endpoint URL construction inline in hooks | Request factories in #/shared/http/endpoints.ts |
requestAndParse() / requestWithThrow() | request() from #/shared/http/request |
useQuery / useInfiniteQuery in components | useSuspenseQuery / useSuspenseInfiniteQuery in components |
| No data prefetching | queryClient.ensureQueryData(queries.xxx()) in route beforeLoad |
document.title in useEffect | head() function on the route |
refetchInterval for polling | Same — refetchInterval on the query options |
The pattern is: define the endpoint in endpoints.ts, wrap it in queryOptions in queries.ts, prefetch in the route loader, consume with useSuspenseQuery in the pod page component. This replaces the legacy pattern where each feature scattered its own query hooks.
This is where most legacy code diverges from the target. Legacy apps use MobX stores for everything — server cache, UI state, filters, form drafts. The target splits state across purpose-built mechanisms:
| What the MobX store holds | Target mechanism | Why |
|---|---|---|
| Server data (fetched from API) | TanStack Query via queries.ts | Automatic caching, deduplication, background refresh, garbage collection |
| Filters, sort order, pagination cursor | URL search params with Zod validateSearch | Linkable, shareable, survives refresh, back/forward navigation works |
| Selected items, active tab | URL search params | Same reasons — if the user can share the URL and expect the same view, it belongs in the URL |
| Modal open/close, menu open, input draft | useState | Ephemeral — losing it on navigation is fine |
| Authentication session | MobX store (authentication.store.ts) | Already exists in the target, keep it |
| Theme preference | MobX store | Already exists in the target, keep it |
When you encounter a MobX store in legacy code, don't port it. Decompose it by asking: "Would a user want to bookmark or share this state?" If yes → URL. If it's server data → TanStack Query. If it's ephemeral → useState.
| Legacy | Target |
|---|---|
styled-components (styled.div, styled(Tile)) | SCSS Modules (.module.scss) |
css helper from styled-components | Regular SCSS |
Transient props ($isActive) | CSS class toggling or data attributes |
@carbon/elements styles.productiveHeading02 | @carbon/type SCSS mixins (@include type.type-style('productive-heading-02')) |
Hardcoded px / rem values | Carbon spacing tokens (var(--cds-spacing-05), $spacing-05) |
Carbon token CSS vars (--cds-border-subtle-01) | Same — keep using Carbon CSS custom properties |
SCSS modules use dot notation for class access: className={styles.container}.
| Legacy | Target |
|---|---|
export default Component or export {Component} inline | export {Component} block at end of file |
const Component: React.FC = () => ... | function Component() { ... } or const Component: React.FC = () => ... (both OK, function declarations preferred) |
index.tsx as main file in component dir | PascalCase.tsx named file (no barrel files) |
Import via directory (./Dashboard) | Import via file (#/shared/dashboard/Dashboard or #/operate/components/Dashboard) |
React.memo() wrapping | useMemo for derived data inside components; React.memo only when profiling shows a need |
observer() from mobx-react-lite (for MobX) | Remove — no MobX observation needed for server state or URL state |
| Legacy | Target |
|---|---|
| Try/catch in components | errorComponent on the route handles render errors |
| Manual 401 checks | Centralized in request() — clears query cache, disables session |
notificationsStore.displayNotification() | Build a notification module when needed, or use Carbon's InlineNotification inline |
<ErrorBoundary> per route | errorComponent per route (TanStack Router's built-in mechanism) |
This is the biggest divergence. Legacy tests use jsdom + Testing Library globals + vi.mock(). The target uses a real Chromium browser + MSW-only mocking + no global screen.
| Legacy | Target |
|---|---|
import {render, screen} from 'modules/testing-library' | import {it} from '#/vitest-modules/test-extend' + import {render} from 'vitest-browser-react' or renderWithRouter |
screen.findByRole() / screen.getByRole() | screen.getByRole() from render return value |
await screen.findByText('...') | await expect.element(screen.getByText('...')).toBeVisible() |
waitFor(() => expect(...)) | await expect.element(...) — retries automatically |
screen.queryByText('...') for absence | await expect.element(screen.getByText('...')).not.toBeVisible() |
vi.mock('../../stores/auth') | MSW endpoint mocks — mock the HTTP, not the module |
nodeMockServer.use(http.post(...)) | worker.use(mockEndpoint({successResponse: ...})) |
mockFetchProcesses().withSuccess(data) | mockProcessesEndpoint({successResponse: HttpResponse.json(data)}) |
MemoryRouter + QueryClientProvider wrapper | renderWithRouter('/path') — creates real TanStack Router + QueryClient |
getMockQueryClient() | Built into renderWithRouter |
user.click() from userEvent | screen.getByRole('button').click() — direct locator interaction |
// given / when / then comments | No section comments — use blank lines for visual grouping |
Endpoint mocks live in shared-test-modules/mock-handlers.ts. If the mock you need doesn't exist, add it there using createEndpointMock() — never create mocks inline in test files.
Legacy (Operate): No i18n — all strings are hardcoded in English. There are no i18next imports, translation files, or useTranslation() calls.
Target: The orchestration cluster webapp uses i18next + react-i18next. When migrating from Operate, wrap user-facing strings in t('key') via useTranslation() and add translation keys to the appropriate namespace in src/shared/i18n/. Check the target's namespace structure before adding keys.
The orchestration cluster webapp does not use Mixpanel tracking. Do not migrate legacy tracking imports, event calls, event types, tracking-only state, configuration, or tests. When tracking shares a callback with feature behavior, remove only the tracking code and preserve the functional behavior.
Not every migration is a full page. You might be adding a single component, column, filter, or panel to a page that already exists in the orchestration webapp.
For partial work:
queries.ts and the endpoint to endpoints.tsPorting MobX stores verbatim. This is the most common mistake. Legacy stores mix server cache, UI state, and derived data in one class. In the target, these concerns are separated. Decompose the store, don't port it.
Scattering query definitions. Legacy apps have use*.query.ts files next to each feature. The target centralizes all queries in #/shared/http/queries.ts and all endpoints in #/shared/http/endpoints.ts. This makes the full API surface visible in one place.
Using vi.mock(). Legacy tests mock MobX stores and modules with vi.mock(). The target mocks HTTP responses with MSW. If you need to control what a component sees, control it at the network level. The only acceptable use of vi.mock() is for things like vi.useFakeTimers() where there's no HTTP-level alternative.
Creating inline MSW handlers in test files. Don't write http.post('*/v2/...', () => HttpResponse.json(...)) directly in tests. All endpoint mocks must be defined in shared-test-modules/mock-handlers.ts using createEndpointMock() and consumed via worker.use(mockXxxEndpoint({successResponse: ...})). If the mock you need doesn't exist yet, add it to shared-test-modules/mock-handlers.ts — never inline it.
Using waitFor or findBy*. These don't exist in Vitest browser mode. Use await expect.element(...) which retries automatically.
Creating barrel files. The target doesn't use index.ts barrel files. Import files directly using #/ path aliases.
Putting fetch logic in page components. Data fetching belongs in route loaders (beforeLoad or loader). Page components consume data via useSuspenseQuery, they don't initiate fetches.
Using styled-components. Even if the legacy code uses them, the target uses SCSS modules exclusively. Rewrite, don't port.
Checking isLoading / isError on useSuspenseQuery. Unlike useQuery, useSuspenseQuery never returns loading or error states — it suspends the component (handled by <Suspense> or the route's pendingComponent) and throws on errors (handled by errorComponent). If you need inline loading/error handling within a component, use useQuery instead, but prefer useSuspenseQuery with route-level boundaries as the default.
Porting useEffect for navigation. Legacy code uses useEffect(() => navigate(...)) for conditional redirects. The target uses throw redirect() in beforeLoad — it's synchronous relative to the route lifecycle and avoids the flash of the wrong page.
docs/monorepo-docs/frontend/orchestration-cluster-webapp.md — tech stack, layout, scriptsdocs/monorepo-docs/frontend/data-loading.md — TanStack Router + Query patternsdocs/monorepo-docs/frontend/forms.md — form library guidancedocs/monorepo-docs/frontend/testing.md — unit, integration, a11y, visual testingdocs/monorepo-docs/frontend/code-style.md — naming, exports, commentsdocs/monorepo-docs/frontend/development-process/creating-a-new-page.md — page creation checklistdocs/monorepo-docs/frontend/development-process/before-starting.md — pre-feature considerationsdocs/monorepo-docs/frontend/legacy-components.md — legacy app overview + migration epic linkreferences/pattern-mapping.md — detailed side-by-side code examples for every transformationPublished skills whose descriptions closely match this one, with their headline benchmark lift.
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.