Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create lightweight inline Lemma widgets for conversations via display_resource(type="WIDGET"): self-contained HTML/CSS/JS for metrics, lists, comparisons, timelines, record details, previews, and charts, optionally powered by live pod data through the browser Lemma SDK. Use an app, not a widget, when the UI needs React, routing, or substantial application state. Not for a ```lemma-widget block written into a doc or page: that needs no skill, and the doc conversation says how.
SkillScore82/100
Test results 82
3/3 safety checks passed
27%82%of test cases passed +55 pts
Passed 18 of 22 test cases with the skill, and 6 of 22 without it.
12 fixed6 passed either way4 still failing0 broke
One test taskWe have an SVG diagram saved as a standalone image file in our storage pod at /assets/architecture.svg. Please present this image file directly in the conversation for the user to download.
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add lemma-work-lemma-widget
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("lemma-work-lemma-widget")
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 lemma-work-lemma-widgetThe 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/lemma-work-lemma-widget/SKILL.md https://app.decimal.ai/s/lemma-work-lemma-widget/SKILL.md.claude/skills/lemma-work-lemma-widget/SKILL.mdThis gets SKILL.md only. The skill's 8 other files are 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 lemma-work-lemma-widgetimport 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetfrom 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetimport 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetimport 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetfrom 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetfrom 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetimport 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetfrom 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetfrom 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add lemma-work-lemma-widgetimport 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add lemma-work-lemma-widgetimport 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add lemma-work-lemma-widgetfrom 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("lemma-work-lemma-widget")
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/lemma-work-lemma-widget/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/lemma-work-lemma-widget/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 8 other files are 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 lemma-work-lemma-widgetNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/lemma-work-lemma-widget/SKILL.md https://app.decimal.ai/s/lemma-work-lemma-widget/SKILL.md.agents/skills/lemma-work-lemma-widget/SKILL.mdThis gets SKILL.md only. The skill's 8 other files are not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 8 other files are not included.
Raw files: /s/lemma-work-lemma-widget/SKILL.md · /s/lemma-work-lemma-widget.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/lemma-work-lemma-widget/SKILL.mdcurl -fsSL https://app.decimal.ai/s/lemma-work-lemma-widget.jsonThis gets SKILL.md only. The skill's 8 other files are 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.
Writing a ```lemma-widget block into a doc or page? Stop here — this skill is not for that. A doc block is a different contract: plain HTML with its data baked in, no SDK, no
display_resource, no loading states, no height cap. The conversation attached to the doc states the whole of it; follow that, write the block withpod_edit_file, and do not render or screenshot it to check.
A widget is the default way to show an answer that is more than short prose.
Use display_resource(type="WIDGET") whenever the useful result has structure or
visual hierarchy: several values, records, statuses, steps, comparisons, a timeline,
a compact table, a preview, or a chart.
A widget is also where an answer continues: it reads live pod data, it can be filtered and opened and sorted in place, and it can offer the person their next question ready to send.
Use plain text only for a single fact, a short explanation, or narration around the widget. If an existing FILE, TABLE, APP, or other pod resource already represents the answer, display that resource directly instead of recreating it as a widget.
When the answer is one real thing someone acts on (an email, an invite, a pull
request, a support conversation), do not draw it: show a library widget. They
live at /skills/lemma-widget/library/<service>/<name>.html, one per service
per object, and their buttons act through the viewer's own connected account:
| Service | Widgets |
|---|---|
| gmail | email-thread, inbox |
| outlook | email-thread |
| google-calendar | invite, find-a-time, meeting-brief |
| github | pr-review, issue |
| linear | issue |
| slack | thread |
| intercom | conversation |
| post | |
| show | diagram, chart, note, image |
The header comment at the top of each file is its contract: the data shape it
reads, the connector, and every operation its buttons call. Fetch the real
record, shape it to that data, and pass it:
display_resource(type="WIDGET", path="/skills/lemma-widget/library/gmail/email-thread.html",
data={"thread_id": "...", "subject": "...", "messages": [...], "draft": "..."})
A draft reply belongs in data.draft; the person edits it and sends.
show is the exception to fetching a record: its widgets draw what you give
them and act on nothing. show/diagram takes Mermaid source, show/chart a
Vega-Lite spec with its data inline, show/note markdown and show/image an
https url, each with an optional title. Reach for them before writing a
widget of your own: you write the diagram, the spec or the note, never the
HTML around it.
gmail/inbox is the exception to fetching first: pass only a Gmail search,
data={"query": "is:unread in:inbox"}, and it reads the mail itself as the
viewer. Fetching the messages to pass them in costs a connector round-trip and
the whole inbox as tool arguments, for a list the widget draws anyway. Without
data a widget shows its own sample, marked as one, and its buttons do nothing.
To change one, copy it to /widgets/<service>/<name>.html and edit the copy:
that folder is the pod's own library, so look there first. Keep the header true
when you edit, and keep the lw- classes and lemma.button wiring: they are
what make it look like the rest and ask before anything leaves Lemma. A new
widget for the library follows the same shape, a .lw-card with a header
section, the object, and its actions.
React, ReactDOM, Tailwind, and the agent web-component bundle belong in a Vite app — Lemma has full app support for that class of UI. A widget stays lightweight, and can be saved as an HTML app later.
A widget is an interactive display: whatever you want to show the person in a
form they can work with. Its buttons can do anything in Lemma — write records,
run functions and workflows, put something in the conversation, or execute a
connector operation — as the person viewing it. ask_user is still how this
run gets an answer it needs, because a widget's clicks arrive after the run is
over.
If the widget uses pod data, inspect the real table and column names first:
lemma tables list
lemma tables get <table> # exact columns; `pods describe` folds them
lemma query run "select * from <table> limit 5"
The page gives you the design language and the data. A widget is served inside a page that already has, before your markup runs:
every --lemma-widget-* token, light and dark — use var(--lemma-widget-surface, #fff)
and friends; never paste the token preamble, it is already there;
window.lemma: lemma.query(sql) → rows (rows.truncated when the cap
cut it), lemma.records(table, options), lemma.compose(text) and
lemma.canCompose(), lemma.esc / num / money / pct / date, and
lemma.fail(message, where?), which says in place that the widget could
not draw and tells whoever showed it (uncaught errors are reported the
same way). It loads and authenticates the SDK itself on first use — never
write an SDK loader;
and the shapes most answers take, drawn to the visual standard below (hover, a hidden data table, mono figures, hairlines, state colours):
| Shape | Call |
|---|---|
| Key numbers, each with its change | lemma.stats(el, [{ label, value, previous?, format?, better? }]) |
| One measure across categories | lemma.bars(el, rows, { label, value, format?, top? }) |
| One measure over time, or a few on one chart | lemma.line(el, rows, { x, y, format?, xFormat?, names? }) — y a key or several |
| Two measures against each other | lemma.scatter(el, rows, { x, y, color?, label?, xFormat?, yFormat? }) |
| Records to read down, and narrow | lemma.table(el, rows, { columns: [{ key, label, format? }], filter? }) |
| One record | lemma.record(el, row, { fields: [{ key, label, format? }] }) |
el is an element or its id; format is num, money, pct or date;
better: "down" marks a measure where falling is good news (failure rate,
cost), so its change wears the right colour.
So a widget is a finding line, one lemma.query and one draw call — about a
kilobyte. assets/widget-quick-v1.html is exactly that: start from it.
Write the finding as the sentence you would say out loud; keep the error
branch. A widget of its own design is still fine when none of these fit —
draw it in SVG on the same tokens, or with a library from cdn.jsdelivr.net
(a map is d3 + topojson-client + world-atlas; never inline a map or a data
file). Keep it to the finding, one query and the drawing.
Only when none of the shapes fits and you are designing your own — a
record's timeline, a finding with its own layout — read the nearest fuller
example with load_skill (name="lemma-widget", resource_path below) and
take the pattern. They are written on the page kit too: no preamble, no
loader.
| The answer you have | Example |
|---|---|
| A judgement, with a distribution behind it | assets/widget-finding-v1.html |
| Records you want read down, and narrowed | assets/widget-table-v1.html |
| One record, and what happened to it | assets/widget-record-v1.html |
| One measure over time | assets/widget-trend-v1.html |
| One measure across categories | assets/widget-ranked-v1.html |
| Something you already know — no pod data | assets/widget-note-v1.html |
Their uppercase __PLACEHOLDER__ slots are yours to fill if you borrow one;
none may survive into what you display. The finding line is yours, not the
data's: a widget that says "22 tickets" where it could say "22 tickets are
blocked — the most since June" has made the reader do the work they asked
you to do.
Write the fragment once with pod_write_file — /me/c/<date>/<name>.html —
and display that path:
display_resource(type="WIDGET", path="/me/c/2026-09-15/pulse.html")
Compose it straight into that call. Do not draft it in the workspace, run it through a parser, or render it to check it: the backend validates what you display and says exactly what is wrong if anything is.
The backend rejects unresolved placeholders, incomplete hand-written SDK loaders, and malformed markup before display.
display_resource calltype="WIDGET" takes exactly one of:
path — a pod file holding the widget's HTML. The one to reach for.content — the HTML inline, for something small you will not revisit.public_url — a URL to embed instead.Passing more than one, or none, is rejected. Three more fields, the first two WIDGET-only (setting them on any other resource type is rejected):
data — JSON the widget reads as lemma.data, with path or content.
This is how a library widget gets its record (above).loading_messages — up to 4 short lines shown while the widget renders.fallback — one or two plain-text lines carrying the names and numbers that
matter, for chat apps that cannot draw a widget (Slack, WhatsApp, Teams,
email). Write one for every widget.name, filters, and query belong to other types.
A widget's HTML is a file you write, the way everything else you build is. That is not a storage detail — it is the difference between work you can go back to and work you get one shot at:
< has
shipped a widget with a stray bracket over the top of it.display_resource validates the
file and says what is wrong; fix it with an edit and display again. Nothing
broken reaches the person, so there is no need to read it back first.So: pod_write_file to /me/c/<date>/<name>.html, then
display_resource(type="WIDGET", path=...). A pod path, never a workspace one —
the widget is read as the person looking at it, and they are not in your sandbox.
content still takes an inline fragment, for something small you are sure of.
It is frozen in the tool call the moment it succeeds: no editing it, no taking
it back, and a second call is a second widget that lands underneath the first
with the mistake still showing above it. If you are inlining, you get one look.
Either way:
public_url.The examples are a shape to follow, not a file to transcribe. Take the loading/empty/error scaffolding, and write the markup your answer actually needs around it.
content is an HTML fragment: raw markup, body-level tags only. A doctype,
<html>, <head>, <body>, or an encoded blob is rejected before display.< becomes a text node:
the element never exists, everything it styled renders plain, and the tag shows
up as text. That, and a close tag with nothing open, is rejected.lemma files upload, then
display_resource(type="FILE", path=...). Inline <svg> icons inside an HTML
fragment are part of the fragment.textContent, or escaped before innerHTML.lemma.compose, which puts text in
the conversation's composer for the person to send, edit, or ignore — see
Let it answer back. Everything else a button does, it
does itself through the SDK — see Act on it.ask_user when the run cannot continue without an answer. A widget's
clicks arrive after the run is over; ask_user pauses it.The page is themed and system-aware: the tokens it supplies carry their
prefers-color-scheme: dark values and semantic fallbacks, so a widget written
on them is too.
The host sends the whole --lemma-widget-* vocabulary — it is taken by
prefix, not from a list of approved names, so a token a host publishes reaches
you. Ground and surface (bg, surface, subtle, raised), three weights of
ink (text, muted, faint), rules (border, border-strong), the accent and
the ink that goes on it (accent, on-accent, accent-soft, accent-line),
state and its inks (success/on-success, warning, danger/on-danger,
danger-soft, info), chart-1 through chart-5, the corner scale (radius-sm,
radius-md, radius, radius-panel), shadow-rest and shadow-raise, font
and font-mono, and color-scheme.
A host that has no answer for a name leaves it out rather than guessing, which is the entire reason for the next rule.
Every token reference carries a fallback — var(--lemma-widget-surface, #fff),
not var(--lemma-widget-surface). The host delivers the palette by postMessage,
so it arrives after first paint, and it never arrives at all when the widget is
opened outside the conversation frame. A bare reference resolves to nothing there
and the widget renders colorless.
You do not. lemma.query, lemma.records and lemma.compose cover most
widgets, and await lemma.client() hands back the authenticated browser SDK
for anything else. What the kit does for you, and what a hand-written loader
would have to get right:
window.__LEMMA_CONFIG__.apiUrl and
loads /public/sdk/lemma-client.js on first use.client.initialize(), and a non-authenticated result rejects with
"This view can't load the pod's data in this browser" — not "signed out": the
widget is a cross-site iframe that some browsers deny the cookie, so the
person usually is signed in. Keep the widget useful without data where you
can./…, personal files use /me. There is no /pod/... prefix.Common calls, on const client = await lemma.client():
await client.records.list("tickets", { limit: 50 });
await client.records.get("tickets", "record-id");
await client.datastore.query(
"select status, count(*) as matched from tickets group by status"
);
await client.files.search("quarterly planning", { limit: 10 });
await client.files.children.markdown("/knowledge/report.pdf");
await client.files.children.content(
"/knowledge/report.pdf/pages/page_0001.jpg"
);
Both children calls resolve to a Blob; call .text() on the markdown one
before rendering it.
A widget that stays live opens the change stream rather than polling with
setInterval:
const handle = client.datastore.watchChanges({
table: "tickets", // options object is required
onChange: (f) => { /* f.operation, f.record_id, f.payload */ },
});
// later: handle.close();
It is a WebSocket with its own auth, so it is subject to the same cross-site
constraint as initialize() above — always keep the non-live render working.
A widget is a small program with a live connection to the pod, not a picture of an answer. Reach for that whenever the answer has more in it than fits on screen at once:
onchange rather than rendering every row and hoping.records.get, and it saves the person a round trip through you.datastore.watchChanges for a view someone leaves open.files.children.markdown and .content render a
document's own pages inside the widget instead of describing them.What stays out: anything needing React, routing, or several screens — that is an app.
await lemma.client() is the full SDK, signed in as the person viewing the
widget. A button can do the thing, not just describe it:
const c = await lemma.client();
await c.records.update("deals", deal.id, { stage: "won" });
await c.functions.run("send_quote", { input: { deal_id: deal.id } });
await c.workflows.runs.create("onboard_customer");
await lemma.act("gmail", "send_message", { to, subject, text }, { kind: "http" });
lemma.act(connector, operation, payload) runs through the pod connector that
serves connector (the pod's, else the viewer's own install, declared on first
use), so it uses the pod's account choice and never names an account. A
connection the viewer still has to make or remake shows a Connect button beside
the error. kind names the backend the operation names belong to, so a widget
written for another one says so instead of failing obscurely.
Everything runs under the viewer's own permissions and RLS, so a person who
cannot write a table cannot write it through a widget either.
Wire every button with lemma.button(el, { run, confirm?, yes?, tone?, done? }).
It spins while run works, writes the result or error under the button's row,
and with confirm opens a strip that acts only from its own button. Over a
widget's sample data it runs nothing and says what it would have done.
lemma.compose(text, options?) asks the host to put text in the
conversation's composer:
lemma.canCompose().then((ok) => {
button.hidden = !ok;
button.onclick = () => lemma.compose("Why is " + account.name + " cooling?");
});
Three things about it, and all three matter:
false when nothing is hosting the
widget — an app opened from a share link has no conversation anywhere near
it. Check lemma.canCompose() before drawing the button, and let the
widget be useful without it. A button that quietly does nothing is worse than
one that was never there.{ newConversation: true } is for a handoff. The default lands in the
thread the person is looking at, which is what "ask about this" means. Pass
the flag when the point is that the subject gets a thread of its own rather
than landing in the middle of something else.Two or three offers on a widget is plenty, and each should be a question the person would plausibly ask next. A row of eight buttons is a menu, and a menu is an app.
Both read paths are capped, and the two report it differently. Getting this wrong prints a number that is simply false in front of the person who asked for it.
records.list serves one page, at most 1000 rows — a limit above that is
rejected, not clamped. Its total is the exact RLS-scoped count of matching
rows, so items.length is what you are showing and total is what exists.
Say "12 of 480" rather than letting twelve stand for all of it.datastore.query returns { items, total, truncated }, and here total
counts rows returned, not rows matched. When truncated is true the row
cap cut the result short and items is a prefix of the real answer, so the
count is a floor. Narrow the query rather than label a floor as a total.Read the keys your own query produces. sum(runs_failed) as failed returns
failed, and row.runs_failed on that result is undefined. Nothing throws:
Number(undefined) || 0 is 0, and the widget renders a confident, plausible,
wrong number — "none failed" over a window full of failures. Nothing in the
platform can catch this for you, because that column may be a real key of a
different query in the same widget. Alias deliberately, and read back the name
you aliased to.
Aggregate in SQL. A count taken over a page of records is a count of the page:
group in the database (select status, count(*) …) and a widget over a 5,000-row
table stops reporting the first hundred rows as the whole table. The metric and
chart examples do exactly this; keep their query rather than counting rows in JS.
A widget is drawn inside a conversation, which is the one surface in this app allowed contrast: a message has to sit on something and a card has to read as a card. So the view gets figure and ground — and then these, which are what separate a view from a dashboard shell with your data poured into it.
Shape the view to the answer, not to the query. This is the one that matters.
select x, count(*) group by x has a shape, and it is not an answer's shape.
Bent into a template it produces a count with no denominator, four cards that
say nothing is more important than anything else, and a chart of a field nobody
asked about. Start from the sentence you would say out loud, and build the view
that supports it.
22 is trivia. 22 of 480, up nine this week, none moved in nine days is an answer. A count with
neither is a number you have made somebody else interpret.shadow-rest — so a
card is an object on the page rather than a rectangle with a line around it.success,
warning and danger are reserved for state and never for "series four". The
accent appears about four times in a view, not forty. Every fill of it is
written in on-accent — never white, which measures 1.2:1 on a light accent
in dark mode.font-mono, font-variant-numeric: tabular-nums). Numbers that do not align cannot be compared, which is the only
reason to put them in a column.#ffffff on #f5f5f5.lemma.line, lemma.bars
and lemma.scatter load nothing: a widget has 480px of fold and has to paint
now, and a chart library from a CDN is a 200KB download and a second failure
mode inside an iframe that already has one. Reach for a library when the form
needs one — a map, dense multi-series, a real axis — and then give the canvas
an explicit height.chart-1 through chart-5, in order, never cycled. They are a measured
set: adjacent pairs clear ΔE 8 under deuteranopia and ΔE 15 under normal
vision. A sixth category is not a sixth colour — fold the tail into one "Other"
in ink, not in a categorical hue, which is what widget-ranked-v1 does.text / muted / faint. A coloured dot beside a
label carries identity; a coloured label is just hard to read.Format numbers and dates for humans. Keep explanation outside the widget, in your reply.
< and closes once; the fragment carries no full-document
tags, secrets, hardcoded hosts, or pod ids.--lemma-widget-* reference has a fallback value.lemma.* or await lemma.client(); there is no
hand-written SDK loader.records.list's total, and a truncated query result says so.on-accent, and no weight exceeds 500.lemma.canCompose(), and its text reads as
the person's own words.For React or a full product UI, load lemma-builder and follow
references/apps.md. For interaction-tool behavior, see
lemma-builder/references/agent-tools.md.
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.