Install any skill in seconds. Free to start, no credit card required.
Get Started Free →End-to-end Salesforce architecture and trade-off design — multi-org strategy, security and identity (SSO, OAuth, JWT Bearer, SAML, FLS), enterprise data modeling and LDV, solution architecture across clouds, integration patterns (Named Credentials, Platform Events, Bulk/REST API, middleware), governor-limit-aware design, and development-lifecycle/deployment governance.
SkillScore82/100
Test results 82
3/3 safety checks passed
59%82%of test cases passed +23 pts
Passed 18 of 22 test cases with the skill, and 13 of 22 without it.
5 fixed13 passed either way4 still failing0 broke
One test taskA developer added three new custom fields to an existing Quick Action in SFDX and deployed it. The deploy succeeded, but the new fields fail to render in the Salesforce UI even after logging out and clearing browser cache. Explain the root cause and the required fix.
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add toddkasper-salesforce-technical-architect
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("toddkasper-salesforce-technical-architect")
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 toddkasper-salesforce-technical-architectThe 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/toddkasper-salesforce-technical-architect/SKILL.md https://app.decimal.ai/s/toddkasper-salesforce-technical-architect/SKILL.md.claude/skills/toddkasper-salesforce-technical-architect/SKILL.mdThis gets SKILL.md only. The skill's 3 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 toddkasper-salesforce-technical-architectimport 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectfrom 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectimport 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectimport 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectfrom 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectfrom 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectimport 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectfrom 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectfrom 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectimport 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectimport 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectfrom 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("toddkasper-salesforce-technical-architect")
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/toddkasper-salesforce-technical-architect/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/toddkasper-salesforce-technical-architect/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 3 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 toddkasper-salesforce-technical-architectNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/toddkasper-salesforce-technical-architect/SKILL.md https://app.decimal.ai/s/toddkasper-salesforce-technical-architect/SKILL.md.agents/skills/toddkasper-salesforce-technical-architect/SKILL.mdThis gets SKILL.md only. The skill's 3 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 3 other files are not included.
Raw files: /s/toddkasper-salesforce-technical-architect/SKILL.md · /s/toddkasper-salesforce-technical-architect.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/toddkasper-salesforce-technical-architect/SKILL.mdcurl -fsSL https://app.decimal.ai/s/toddkasper-salesforce-technical-architect.jsonThis gets SKILL.md only. The skill's 3 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.
The Salesforce Certified Technical Architect (CTA) is the pinnacle credential in the Salesforce ecosystem. It validates the ability to design and implement secure, high-performance, integrated solutions on the Salesforce Lightning Platform at enterprise scale — and to defend those decisions under challenge before a panel of senior peers. Unlike every other Salesforce certification, the CTA is not a multiple-choice exam; it is a live architectural defense before three to four sitting CTAs who probe trade-offs, attack weak decisions, and score independently across seven domains.
This file is an operational playbook, not an exam outline. Each section states the actual rule, the concrete numbers, the decision criteria, and the anti-patterns to catch in review. When you make an architecture or implementation decision, apply these rules and use them to catch your own mistakes. Verify assumptions against the live org with describe/SOQL tooling before writing a mapper or trusting a UI tool's "success."
Load this skill when… designing cross-cloud or multi-org architecture; reviewing integration patterns (API choice, auth flow, idempotency); evaluating org strategy (single vs. multi-org); reviewing security models at the architecture level (SSO/OAuth, identity federation, FLS design across systems). Not this skill: single-cloud configuration (cases, flows, console) → see the cloud-consultant skills; Apex/LWC coding → see
salesforce-platform-developer-1/salesforce-platform-developer-2.
Verify steps assume nothing about your tooling — use your project's Salesforce MCP connection, the Salesforce CLI (
sf), or the Salesforce setup UI, in that order of preference.
Credential logistics and study path: see references/study-resources.md.
[volatile — verify live] items include: Salesforce governor limit values (they are updated between releases), Bulk API 2.0 daily API allocation formulas, OAuth flow support in External Client Apps vs. Connected Apps, and Data Cloud / MuleSoft feature availability per license edition.[volatile — verify live] or [opinion — house style].Inline volatile tags applied:
[volatile — verify live] — Salesforce adjusts limits between major releases; verify against the current Apex Developer Guide.[volatile — verify live] — Bulk API daily limits scale with license count per a formula Salesforce updates; check the current limits documentation.[volatile — verify live] — ECA Policies tab path and Connected App end-of-support timeline evolve; confirm in your org's current Setup UI. Since Spring '26, new Connected App creation is blocked by default org-wide; ECA is the standard for new integrations.[volatile — verify live] — Data Cloud feature set evolves rapidly; verify feature availability and configuration steps in the current Data Cloud documentation.Stay single-org unless you have a hard reason to split. Split into multiple orgs ONLY for: legal/data-residency separation, distinct business units with zero shared data, or M&A integration timelines. For a small single-entity org, a single Salesforce org is correct and any "let's add an org" instinct is wrong. Splitting costs you cross-org reporting, duplicated security-model maintenance, and middleware to sync shared Contacts.
Treat governor limits as design inputs, not runtime surprises. Key synchronous per-transaction ceilings: 100 SOQL / 50k rows / 150 DML / 10k DML rows / 10s CPU / 6 MB heap / 100 callouts; async doubles CPU (60s) and heap (12 MB). A REST/Bulk integration is bound by API limits, not Apex limits — but any trigger or Flow that fires on the changed record IS bound by these. Bulk-safe design matters even at low volume because backfills re-toggle many records at once. Full limits table → references/study-resources.md. [volatile — verify live]
Document management: files go to object storage, references go to Salesforce. Keep file bytes in external object storage (e.g. S3 with customer-managed encryption) and store only the key/reference in Salesforce. When you DO surface a document inside Salesforce, use ContentVersion → ContentDocumentLink (junction to the record). One ContentDocument can link to many records via multiple ContentDocumentLink rows.
License-type selection. Match license type to access need — wrong type creates sharing constraints you cannot engineer around. Experience Cloud licenses (Customer Community / Customer Community Plus / Partner Community) for external users, not full CRM seats; Platform licenses for internal-only custom-app users; full Salesforce only where standard CRM objects are needed. [volatile — verify live] — verify the current license matrix before committing.
Reporting & Analytics. Schema decisions constrain reportability: discrete columns for anything staff will filter or report on — SOQL cannot GROUP BY a JSON sub-key; master-detail for roll-up summaries (cascade-delete trade-off); custom Report Types for cross-object reporting beyond one hop. Reporting across orgs is an ETL problem. CRM Analytics when native reports hit row-count or real-time limits.
Mobile platform solutions. Decision: Salesforce Mobile App (no-code, standard objects) → Mobile SDK (offline sync, push, branded native) → LWC OSS + PWA (mobile web, no app-store). Mobile surfaces expose the same sharing model, FLS, and governor limits as desktop. [volatile — verify live] — Mobile SDK versions change each release. Expanded trade-off worked examples → references/communication-well-architected.md.
Red flags: second org "for the portal" (sandbox covers it); file blobs as base64 in a long-text field; approval automation that loops record-by-record; defaulting full Salesforce licenses to external portal users; storing reportable data in JSON fields; mobile addressed only with "they can use the browser."
Verify: enumerate the custom-object inventory before assuming an object exists; describe the object to confirm field count and types before writing a mapper.
Object access and Field-Level Security (FLS) are two separate gates. You need both. Granting a profile/permset access to an object does NOT grant access to its fields. The most expensive lesson in this space: SFDX field-meta.xml carries no FLS — a freshly deployed custom field is invisible to everyone, including System Administrator, until a profile or permission set explicitly lists <fieldPermissions>. Symptom: SOQL returns "Invalid field" even with full object access. Fix: every non-required custom field must appear in a permset's fieldPermissions.
Never put <fieldPermissions> on a required field. Salesforce rejects the deploy with "You cannot deploy to a required field" — required fields are always visible/editable, so they must be omitted from fieldPermissions. A permset should list FLS for the non-required custom fields and skip the required ones.
Use Permission Sets / Permission Set Groups, not Profiles, for grants. Profiles are legacy. New access goes in a permission set so it is composable and removable without touching a user's base profile.
JWT Bearer for headless server → SF integration. Server-to-server, no interactive login, no refresh-token storage. Private RSA key in a secrets store (e.g. SSM SecureString), never in env vars, rotated annually.
OAuth flow decision table:
| Scenario | Flow |
|---|---|
| Server-to-server, no user | JWT Bearer |
| Web app with backend secret | Web Server (auth code) + PKCE |
| SPA / mobile, no secret | Auth code + PKCE (never implicit/User-Agent) |
| Machine client with its own SF identity | Client Credentials |
External Client App (ECA) is the default for new integrations (Spring '26+). New Connected App creation is blocked by default org-wide since Spring '26 — both UI and Metadata API; a Support request is required to unblock. Existing Connected Apps continue to work. Use ECA for all new integrations. [volatile — verify live] — see official help article id=005228017 for current status and end-of-support timeline.
ECA permission assignment lives on the ECA, not the permset. The "Assigned Connected Apps" section on a permset page governs legacy Connected Apps only. For ECA: ECA detail → Policies tab → Edit → App Policies → Select Permission Sets. Verify via SetupEntityAccess query — do not trust the UI. Consumer Key is UI-only, not accessible via API.
No PII or sensitive data in logs. Ever. Log record IDs and identity subject IDs only.
Red flags: assuming object access implies field access; <fieldPermissions> on a required field (deploy fails); implicit/User-Agent OAuth flow; trusting a UI tool's "success" on ECA assignment; new Contact-writing field missing from FLS permset; designing a new integration around Connected App creation without confirming it is unblocked in the org (Spring '26 default blocks it).
Verify: after deploying a field, SOQL-select it — "Invalid field" = FLS missing, not deploy failure. Confirm permset assignment via PermissionSetAssignment / SetupEntityAccess, not the UI.
Single object + Type__c discriminator over one-object-per-variant when variants are structurally similar — keeps mapper, IaC, and schema-sync simple. Trade-off: wide object. Pair with Record Types for per-type page layouts.
Discrete columns over JSON when staff need to report. JSON only for genuinely free-form, never-reported nested data.
String field max-lengths from a generated schema, never hand-picked. Derive max(...) from a sync script reading the org's metadata. Never hand-edit the generated files. Apex truncation is a last-line fallback for legacy/direct-API records, NOT a substitute for boundary validation.
Lookup relationshipName must be unique per parent object. Two lookups to the same parent can't share a relationshipName (deploy fails: "Duplicate relationship name"). Use role-specific suffixes. SOQL traversal keys on field name, not relationshipName, so renaming is safe.
Selective SOQL at volume. Filter on indexed fields. A non-selective query over an LDV object hits the "non-selective query against large object" error at ~200k+ rows.
| Need | Choice |
|---|---|
| Roll-up summaries, cascade delete, child must have parent | Master-detail |
| Independent lifecycle, optional parent, reparenting | Lookup |
Red flags: hand-editing a generated schema file; a max() literal not tracing to a generated constant; two lookups to the same object sharing a relationshipName; Apex truncation as the primary length guard.
Verify: describe the target object to read current field length / picklist values before changing a form field; if they differ from the generated file, regenerate. SOQL-spot-check external-ID upsert idempotency (no duplicates on retry).
Declarative-first, escalate to code only when declarative genuinely can't do it. Default to configuration; reach for Apex when you need bulk-safe complex logic, callouts with rollback, or behavior Flow can't express cleanly.
| Need | Use | Avoid |
|---|---|---|
| Field data-entry constraint | Validation Rule | trigger |
| Record-change automation | Record-Triggered Flow | Workflow Rules / Process Builder (deprecated) |
| Screen-guided input | Screen Flow | custom LWC unless UX demands it |
| Scheduled batch | Schedule-Triggered Flow or Batch Apex | future methods in a loop |
| Complex multi-object, callouts | Apex handler / Queueable | overstuffed Flow |
| Approval routing | Approval Process / Flow | hand-rolled status Apex |
One trigger per object, logic in a handler class. Bulk-safe: no SOQL/DML inside a for loop. Query once into a Map, iterate in memory, collect into a List, DML once. Recursion-guard with a static boolean. Approval handlers copying fields to a Contact must be additive — role flags OR-merged, never overwritten to false on re-approval.
Design around the managed package, don't fight it. Managed packages ship their own triggers/automation in namespaced Apex you cannot edit — design your automation to coexist. When a field changes value with no code of yours touching it, suspect managed-package automation first. The fix is usually deactivating the offending rule in Setup, not writing counter-code. See salesforce-nonprofit-cloud-consultant for NPSP specifics.
Quick Action cache-bust trick. Adding fields to an existing Quick Action via SFDX updates the metadata, but the runtime QA cache does NOT invalidate — even after logout/login. New fields are silently absent. Fix: edit any non-field-list metadata on the QA (<description>, <label>, <layoutSectionStyle>) and redeploy; SF treats it as a structural change and flushes the cache.
Red flags: SOQL or DML inside a for loop; more than one trigger on the same object; new Workflow Rule or Process Builder; overwriting a Contact role flag to false on re-approval (must be additive); adding QA fields and not seeing them render (cache, not deploy failure).
Verify: describe the object to confirm a new field exists and is the right type after deploy; query a real Contact to confirm approval automation set role flags and mirrored fields correctly; run a report / SOQL to confirm staff-facing reportability of discrete columns.
Match the pattern to latency + reliability, not to convenience.
| Requirement | Pattern | Salesforce mechanism |
|---|---|---|
| Caller needs an answer now | Request-Reply (sync) | REST/SOAP callout, Composite API |
| Don't block the user; deliver eventually | Fire-and-Forget (async) | Platform Events, future/Queueable |
| Large batch load/extract | Batch Data Sync | Bulk API 2.0 |
| External system pushes into SF | Remote Call-In | inbound REST, Bulk API |
| Near-real-time SF→external on change | Event-driven, no polling | Change Data Capture / Platform Events |
Idempotency via external-ID upsert is mandatory. Upsert keyed on a stable external ID so a retried job does not create duplicates. Any new write path must reuse that key — a late re-link by the same external ID needs no new SF write logic.
Resilience: on SF write failure, persist and alert — do not silently drop. Catch the SF error, write the full payload to durable storage, alert staff, enable manual recovery. 3-try exponential backoff before falling to manual. Never let a user-facing success vanish before reaching Salesforce.
Bulk API 2.0 for multi-thousand-row loads. Daily API allowance scales with licenses — design imports to use Bulk, not thousands of individual REST upserts.
Outbound SF→external callouts: Named Credentials + External Credentials. Never hard-code endpoints or secrets in Apex.
Red flags: row-by-row REST where Bulk API belongs; write path without external-ID key (breaks idempotency); SF write failure with no durable fallback; polling on a timer where CDC/Platform Events would deliver on change; hard-coded endpoints in Apex.
Verify: after a test write, SOQL-confirm exactly one record landed for the external ID; re-run the job and confirm the count stays at one (idempotency proof). A fast JWT smoke chain (auth → describe → upsert idempotency → cleanup) should run after any metadata or cert change.
SFDX is source of truth; deploy from the SFDX project root. All sf project … commands must run from the SFDX project root or they fail with "InvalidProjectWorkspaceError".
Apex requires ≥75% code coverage org-wide to deploy to production (and every trigger must have at least some coverage). Test with Test.startTest()/Test.stopTest() to get a fresh set of governor limits and force async to run; use @TestSetup for shared fixtures; mock callouts with HttpCalloutMock. Tests must assert behavior, not just execute lines.
Treat production Salesforce as off-limits for incidental deploys; do metadata work in a sandbox. Production cutover is a separate, planned, documented event, not an incidental deploy.
Run a smoke test after any metadata, cert, or sandbox change. A fast JWT/metadata smoke script catches the FLS, required-field, relationshipName, and JWT gotchas at the layer they bite, in seconds. Keep a known-good end-to-end baseline and treat regressions as blockers.
Sandbox-bringup gotchas to pre-empt: some org policies require a Public Group (with the admin as member) before sandbox creation. Managed packages (e.g. NPSP) must be installed before metadata deploy in sandboxes that depend on them. Partial-copy sandboxes carrying managed-package data can fire package automation unexpectedly on load.
Destructive changes need a plan. Field deletion, permset removal, and JSON-to-discrete migrations can drop data. Sequence: backfill → verify → destructive deploy → re-verify. Never destructive-deploy before the data it would orphan has been migrated.
Commit-and-push discipline. After each completed logical change: lint + build pass, then commit (Conventional Commit) and push. Never commit broken code. Branch first if on the default branch.
Red flags: sf project deploy from repo root (not SFDX root); incidental change against production; destructive deploy without prior backfill; Apex tests that only execute lines without assertions; skipping smoke test after cert rotation.
Verify: describe/SOQL post-deploy to confirm metadata landed AND is queryable (catches missing FLS that the deploy itself won't surface). Confirm idempotency and role-flag correctness before considering a change done.
Sharing model layers stack, from most restrictive to least. OWDs (org-wide defaults) set the floor; Roles + Hierarchy open upward; Sharing Rules add lateral access; Manual Sharing and Apex Sharing add case-by-case. A common board trap: an OWD of Private plus a Sharing Rule "owned by members of a Role" is correct for lateral grant — but Role Hierarchy alone propagates upward, not sideways.
SSO decision table:
| Scenario | Protocol | Notes |
|---|---|---|
| Enterprise IdP owns all identity (ADFS, Okta, Azure AD) | SAML 2.0 (SF as SP) | Salesforce is the Service Provider; IdP issues the assertion |
| Modern IdP with OIDC support | OpenID Connect | Token-based; SF acts as an OIDC client |
| SF → external app (SF as IdP) | Outbound SAML | Reversed direction — SF issues; external app consumes |
| Legacy / no external IdP | Salesforce identity only | No delegation needed; avoid unless truly standalone |
Delegated Authentication replaces Salesforce's own password check with an external web service you own. Use only when a corporate directory must remain the sole credential store and SAML is not feasible. Endpoint must have an IP allowlist.
Experience Cloud: Guest user OWD controls unauthenticated access; authenticated users follow standard sharing + sharing sets / share groups. Never grant a Guest User write access beyond what the experience explicitly requires — audit finding.
Red flags: confusing SF-as-SP (inbound) with SF-as-IdP (outbound); relying on Role Hierarchy for lateral access (upward only); Guest User with edit/delete on sensitive objects; Delegated Auth endpoint without IP allowlist.
Score every proposal against Trusted / Easy / Adaptable — every design choice is defensible on all three axes; when a trade-off sacrifices one, name it. MuleSoft is enterprise API mesh, not point-to-point glue. Data Cloud owns the Unified Profile when a 360 view across clouds is required. Marketing Cloud sees synchronized CRM copies, not live data — never assume otherwise.
Full worked examples — Well-Architected trade-off framing, MuleSoft vs. Named Credentials decision criteria, Data Cloud Identity Resolution, Marketing Cloud integration patterns, and stakeholder-facing defense communication: references/communication-well-architected.md — load when designing multi-cloud architecture or preparing an architectural defense presentation.
Http.send using the Named Credential returns HTTP 200 without hard-coded URL.SetupEntityAccess query — do not trust the UI "success."
→ gate: SELECT SetupEntityId FROM SetupEntityAccess WHERE SetupEntityId = '<AppId>' AND AssigneeId = '<UserId>' returns a row.https://<instance>/services/oauth2/token, receive an access token.
→ gate: access token returned with no invalid_grant or unauthorized_client error.Workflow 3 — Size an LDV data model (selectivity, skew, indexes, archival): see references/scenarios.md.
Five original scenarios covering the highest-value operational gotchas across CTA domains. Scenarios 3–5 are in references/scenarios.md — load them when working through trigger bulkification failures, ECA assignment gotchas, or multi-cloud Data Cloud decisions.
Scenario 1 — FLS invisible after deploy
Situation: A developer deploys Application__c.ReviewScore__c via SFDX. Deploy succeeds. SOQL immediately returns "INVALID_FIELD: No such column 'ReviewScore__c' on entity 'Application__c'". The field exists in Setup UI.
Competent move: This is the FLS gap, not a deployment failure. field-meta.xml carries no FLS — the field exists but is visible to no one. Add it to the relevant permset's <fieldPermissions> (<readable>true</readable> + <editable>true</editable>), deploy the permset, re-run the SOQL.
Tempting-but-wrong: Re-deploy the field, or check the required flag. Neither helps — the field is present; the missing layer is FLS.
Verify: SELECT ReviewScore__c FROM Application__c LIMIT 1 as the integration user — error = FLS still missing; null result = FLS granted. Query SetupEntityAccess to confirm the permset assignment landed.
Scenario 2 — OWD + Role Hierarchy misread as lateral sharing
Situation: A regional manager needs read access to Opportunities owned by a peer region's role — lateral, not reporting. The architect proposes: "OWD = Private; roles at the same tier; Role Hierarchy propagates access, so her manager sees both regions. Requirement satisfied."
Competent move: Role Hierarchy propagates access upward (parent sees child's records), not sideways. Peer roles don't inherit each other. The correct mechanism is a Sharing Rule ("owned by members of Role X, share to Role Y"). The manager above both regions gains access via hierarchy automatically; peers need an explicit sharing rule.
Tempting-but-wrong: Treating Role Hierarchy as "nearby roles share access" — strictly upward. Designing on this assumption silently breaks lateral access requirements.
Verify: After adding the Sharing Rule, SOQL as a user in the target role for an Opportunity owned by a user in the source role. Confirm the reverse direction does not automatically apply without a reciprocal rule.
for loop — query into a Map, DML once. #1 review red flag.<fieldPermissions> in a permset for every new non-required custom field — SFDX field-meta grants FLS to no one.<fieldPermissions> for a <required> field — deploy will fail.SetupEntityAccess query.max() lengths from a generated constant; never hand-edit the generated schema file.relationshipName (role-suffixed).<description>) and redeploying when QA fields don't render.sf project commands from the SFDX project root only.For org-specific applications of these rules, see a per-org appendix in your own project, referenced from a CLAUDE.md.
For NPSP/nonprofit-specific operational guidance, see salesforce-nonprofit-cloud-consultant.
Using this skill and hit a wall? If you find a claim contradicted by the live system or official docs, a missing rule that cost you a wrong attempt, or a decision this skill gave no criteria for — append an entry in the moment to .skill-feedback/salesforce-technical-architect.md at the project root (create it if absent):
date | skill last-reviewed | claim or gap | what you observed instead | evidence (error text / doc URL / query output) | suggested fix
These are harvested back into the skill via the learning loop. When the live system and this file disagree, trust the live system.
[volatile — verify live] marks, executable workflows, tool-agnostic verify steps, and the feedback protocol above. Exam logistics relocated to references/study-resources.md; last-reviewed set to 2026-06-09.Independent educational content to upskill AI agents. Not affiliated with, authorized by, endorsed by, or sponsored by Salesforce, Inc. or any certification body. "Salesforce," "Salesforce Certified Technical Architect," "CTA," "NPSP," "Agentforce," "MuleSoft," "Data Cloud," and all related trademarks and product names are the property of their respective owners and are used here solely for identification purposes. Content is provided as-is for guidance only — verify all rules, limits, fees, and procedures against official Salesforce documentation and your live org before acting. No certification outcome is implied or guaranteed. Governor limits, exam fees, and product capabilities are subject to change; check the official exam guide and release notes for current values.
Automatically, when a task matches. Use when designing or reviewing cross-cloud architecture, integration pipelines, access models, or org strategy. Not single-clou
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.