I get some version of the same message every time I post about Agent Script: "okay, I get what the blocks *are* — config, subagent, actions — but how do I actually go from a blank file to something that works?" Fair question. My last post on this broke down the anatomy of a script. This one is the thing I wish existed when I opened my first blank .agent file and just stared at it for ten minutes.
We're building a real agent here — an internal IT Helpdesk assistant that resets passwords and files tickets — starting dead simple and adding complexity one block at a time. By the end you'll have a working two-subagent script and, more importantly, you'll know exactly what to Google when yours breaks.
TL;DR: We build an IT Helpdesk agent from a blank file — one subagent that handles password resets with identity verification, a second that escalates to a human when things get messy. Along the way you'll see every core block in context (not just defined in a table), plus the beginner mistakes that eat the most time and how to actually validate a script before you publish it.
Before You Open VS Code
You don't need much, but you do need these three things or you'll be debugging the wrong problem:
- An Agentforce-enabled org (a Developer Edition org works fine for this)
- A Salesforce DX project with Agentforce DX installed in VS Code
- An authoring bundle generated for a new agent — retrieve or create one and you'll get a file at
force-app/main/default/aiAuthoringBundles/<Agent_API_Name>/<Agent_API_Name>.agent
That .agent file is a blank canvas the moment it's created. Let's fill it in.
Step 1: The Skeleton Every Agent Needs
Before any logic, an agent needs to know who it is. That's the config and system blocks — and honestly, this part trips up more beginners than the actual logic does later, because it's tempting to skip straight to the "interesting" part.
config:
developer_name: "IT_Helpdesk_Agent"
agent_label: "IT Helpdesk Agent"
agent_type: "AgentforceServiceAgent"
default_agent_user: "helpdesk_agent_user@yourorg.com"
description: "Helps employees reset passwords and file IT tickets, escalating anything sensitive to a
human technician."
system:
welcome: "Hi, I'm the IT Helpdesk assistant. Are you here about a password reset or something else?"
error: "Something went wrong on my end — let me get a technician to help you directly."
Two things worth internalizing here, because they'll save you later:
default_agent_useris not decorative. It's the actual Salesforce user whose permissions the agent runs with. If that user can't see a field or object, neither can your agent — no matter what your Apex action says.errorisn't a fallback you'll never see. You will see it. Write it like a real message a stressed-out employee would actually want to read, not "An error has occurred."
Step 2: Give It Memory — Variables
Right now the agent has a personality but no memory. It can't track anything across the conversation yet — like whether the employee has actually verified who they are. That's what variables is for.
variables:
is_verified: boolean
employee_id: string
reset_attempts: number
Think of these as the agent's short-term memory for this one conversation. Anywhere else in the script, you reference them as @variables.is_verified. This matters more than it sounds like it should — it's the difference between an agent that "remembers" verification happened versus one that re-asks the same question because it forgot three messages ago.
Step 3: Your First Subagent — Password Reset
This is where the actual behavior lives. A subagent is a self-contained chunk of logic — its own description, its own reasoning, its own available actions.
subagent password_reset:
description: "Verifies employee identity and resets their Salesforce password."
reasoning:
instructions:
"Ask for the employee's work email if not already provided ->
If @variables.is_verified is false | Send a verification code and ask the
employee to read it back before doing anything else.
If @variables.reset_attempts > 2 | Stop and hand off to a human technician,
do not attempt another verification code.
Otherwise | Reset the password using the reset action and confirm
completion in plain language."
actions:
- verify_employee_identity
- reset_password
actions:
verify_employee_identity:
description: "Sends a verification code to the employee's registered email and checks it against their input."
target: apex://VerifyEmployeeIdentityAction
reset_password:
description: "Resets the employee's Salesforce password once identity is verified."
target: apex://ResetPasswordAction
Read the reasoning.instructions block slowly, because it's doing two different jobs at once and beginners usually only notice one of them:
- The part before the
->is natural language — the LLM interprets it, same as a prompt. - The
If @variables.xlines after it are hard conditionals, evaluated against real variable values. The model isn't "deciding" whetherreset_attemptsis greater than 2 — it's a fact, checked deterministically.
That distinction is the entire reason Agent Script exists instead of just writing a longer prompt. A prompt can be talked out of a rule. A conditional can't.
Step 4: The Entry Point — start_agent
Right now you've got a subagent that works, but nothing tells the conversation to start there. Every script needs exactly one entry point:
start_agent agent_router:
label: "Helpdesk Router"
description: "Determines whether the employee needs a password reset or general IT support."
reasoning:
instructions:
"If the employee mentions a password, login, or being locked out ->
hand off to Password_Reset_Agent.
Otherwise | ask a clarifying question about what kind of IT issue they're having."
actions:
go_to_password_reset: @utils.transition to @subagent.password_reset
Forget this block and your agent will validate fine but do absolutely nothing useful when a real conversation hits it — there's no door for the conversation to walk through. This is, without exaggeration, the single most common "why isn't my agent doing anything" issue I see from people just starting out.
Step 5: Add a Second Subagent — Escalation
Now let's make it feel like a real production agent instead of a demo. Three failed verification attempts shouldn't just dead-end — it should hand off to a human, cleanly.
subagent escalation:
description: "Hands the conversation to a human IT technician when automated verification fails or the issue is
out of scope."
reasoning:
instructions:
"Apologize once, briefly — don't over-explain the failure.
Confirm the employee's issue in one sentence so the technician has context.
Transition to a live agent immediately after that confirmation."
actions:
- transition_to_human
actions:
transition_to_human:
description: "Hands off the conversation to a human technician queue."
target: flow://EscalateToHumanTechnician
Notice this action points at flow:// instead of apex://. That's deliberate, not a typo — Agent Script doesn't care whether the actual work happens in Apex or Flow, it just needs a target it can invoke. Use whichever your team already has, or whichever is easier for someone else to maintain after you.
How a Message Actually Moves Through This
This is the mental model that made everything click for me — worth having open in a tab while you're still getting a feel for how these blocks talk to each other.
Beginner Mistakes That Waste the Most Time
I've made every one of these myself, and I've watched other people make them too. Save yourself the twenty minutes of confused staring:
| What Goes Wrong | Why It Happens | Fix |
|---|---|---|
| Agent validates fine but never responds | No start_agent block, so there's no entry point for the conversation | Every script needs exactly one start_agent |
| Agent keeps re-asking a question it already got answered | Answer was never stored in a variable — the model has no memory of it | Capture it in variables and reference with @variables.name |
| "Never do X" instruction gets ignored occasionally | Rule was written as prose in reasoning.instructions instead of an explicit conditional | Move hard rules into If @variables.x conditionals, not just sentences |
| Validation fails with no obvious reason | Usually a missing colon, wrong indentation, or a typo in a block name | Run AFDX: Validate This Agent after every small change, not just before publishing |
| Action never fires | The action's target doesn't match the real Apex method's @InvocableMethod name, or it's not listed under the subagent's actions | Double-check the target path and that it's referenced in both places |
| Confusing "topic" vs "subagent" in docs or old code | Salesforce renamed topics to subagents — same concept, different word depending on version | Just know they mean the same thing; don't rewrite working scripts over naming alone |
Practices Worth Building Now, Not Later
- Validate constantly, not just at the end. Run
AFDX: Validate This Agentafter every meaningful edit. Catching a typo in a 10-line script takes ten seconds; catching it in a 200-line script takes ten minutes. - Put hard rules in conditionals, not prose. If something must never happen — skipping verification, exceeding a retry limit — express it as an explicit
If @variables.xcheck. Anything left as a sentence is a suggestion, not a rule. - Name variables like you'll forget what they mean in six months. Because you will.
reset_attemptsbeatsxevery time. - Keep each subagent doing one job. The moment a single subagent's reasoning block starts handling three unrelated situations, split it. It's the same instinct as keeping an Apex method focused — easier to read, easier to test, easier to hand to someone else.
- Preview before you publish. Test the actual conversation flow from the script file itself. Reading the logic and watching it run are two very different confidence levels.
- Commit it like real code, because it is real code. The whole point of Agent Script is that it lives in your DX project — treat it with the same code review standards as an Apex class, not as a config file nobody looks at twice.
Where to Go From Here
You've now got a two-subagent agent with memory, a real conditional rule, and a clean human escalation path — which honestly puts you past where a lot of "finished" demo agents stop. The natural next step is chaining in a connected_subagent to hand off to a completely separate Agentforce agent, or grounding a subagent's answers in real data with RAG instead of a single Apex lookup. Both are just more of the same building blocks, arranged with a bit more intention.
Building your own agent right now and stuck on something specific? Drop it in the comments — I read every one, and "my conditional isn't firing" is usually a five-minute fix once I can see the actual script.













