Skip to content

Latest commit

 

History

History
585 lines (436 loc) · 31.6 KB

implementation-guide.md

File metadata and controls

585 lines (436 loc) · 31.6 KB

Typeback: implementation guide

Written for anyone rebuilding Typeback in their own Salesforce org, whether that's an AI coding agent working in this repo or a person doing every step by hand.

This guide covers every step we took to get Typeback working on a live phone call, in the order to do them, and every problem we hit along the way. The day-by-day story is in build-log.md. This guide is the recipe.

What you end up with: a phone number that reaches an Agentforce Voice agent, a resort concierge. When the caller wants something emailed, the agent makes a one-field form link instead of asking them to spell their email. The caller types it on their phone and says "done". The agent reads back a masked hint ("starting with c, at example dot com") on the same call. The agent's language model never sees the full email.

What's proven: the whole loop on a live call from a mobile phone. What isn't: texting the link automatically. This org couldn't send SMS (see SMS), so the link reaches the phone by hand.

How to use this guide

Every step has one tag:

Tag Meaning
[HUMAN] A person has to do it: a Setup click path with no CLI equivalent we used, something that needs a phone, or something outward-facing or hard to undo.
[AI] A command in this repo. An AI agent can run it, and a person can paste the same command into a terminal. Each one says what output means it worked.

Doing it all by hand? Run the [AI] commands yourself, in order. Nothing in them needs an AI.

Handing it to an AI agent? Give it this file and a prompt like: "Read docs/implementation-guide.md and follow it from Phase 0. Run the [AI] steps yourself. At each [HUMAN] step, stop, tell me exactly what to do, and wait until I say it's done." Also give it the rules below.

Rules for an AI agent

  1. Stop at every [HUMAN] step. Say what to click, then wait. Don't look for a scripted way round it.
  2. Never do these without the person's go-ahead at that moment: activate a site, get a phone number, register an SMS brand or campaign, send an email or SMS (even to the person), or delete anything in the org.
  3. Keep secrets out of output. sf org display prints an access token, so use --json with jq and pick out only the fields you need. Never print tokens, session IDs or login URLs. Show phone numbers as their last four digits only.
  4. Keep org details out of committed files. That means usernames, org IDs, domains and phone numbers. .env.local and org-reference/ are gitignored, so those details can go there.
  5. An SMS provider's secret is typed in by the person only, at a hidden prompt or in Setup. Never ask for it in chat.
  6. Before any commit, put the agent user placeholder back: node scripts/setup-agent-user.js --username __AGENT_USER_PLACEHOLDER__
  7. Live form links are bearer links. Don't paste one anywhere public: it works until it's used or expires.
  8. Run scripts by path, for example /bin/bash scripts/report.sh. Each one moves to the repo root and reads .env.local itself. Pass --json to sf commands in non-interactive shells, because their progress display floods logs otherwise.
  9. Describe before you query. Some fields you'd expect aren't queryable (for example MessagingChannel.SessionHandlerType and VoiceCall.IsVoiceMail). Check with sf sobject describe -o typeback -s <Object> --json | jq -r '.result.fields[].name'.

How it works

Caller's phone ──call──▶ phone number ──▶ Omni-Channel flow (Route Work) ──▶ TypebackDemoVoice (Agent Script)
                                                                                     │
         "Can you email me my booking confirmation?"                                 ▼
                                                            TypebackRequestAction: makes a token, saves only
                                                            its hash on a Typeback_Request__c row (Pending),
                                                            and hands the link to the sender
                                                                                     │
               Log sender: keeps the link on the row (Test_Link__c); scripts/link.sh prints it
               (an SMS sender would text it here instead)
                                                                                     ▼
         Caller opens  https://<site>/typeback/?t=<token>  ──▶  typebackForm (LWC, guest site)
                                                          ──▶  TypebackFormController: row → Submitted
                                                                                     │
         Caller says "done" ──▶ router guard runs TypebackCheckAction before any LLM pass
                            ──▶ "I have an email starting with c, at example dot com. Is that right?"

The design rules that matter if you change the code:

  • The agent actions never throw and never return nulls. Every Typeback line the agent says is written in Apex and spoken word for word.
  • Only the token's SHA-256 hash is stored. The link carries a 128-bit random token.
  • The agent user can't read the typed value. The Typeback_Agent permission set has no access to Value__c, and a test proves it.
  • Send before saving. Apex can't make a callout after DML in the same transaction.
  • Links are tied to their call. The call Id comes from the agent's VoiceCallId linked variable, never from what the caller says.
  • Pull, not push. There's no API to push an event into a live voice session, so the agent checks for the typed value on the caller's next utterance.

Where things are: see "What's here" in the README. The agent is force-app-agent/main/default/aiAuthoringBundles/TypebackDemoVoice/TypebackDemoVoice.agent.

Prerequisites

The org needs all of these. Phase 0 checks them.

  • Agentforce, with Agentforce Service Agents
  • Agentforce Voice (Agentforce Contact Center), with a way to get a phone number. Our org's entitlements included one 10-digit long-code number and inbound voice credits, so the number cost nothing.
  • A free Einstein Agent User license, for the agent user
  • Digital Experiences, for the guest site that hosts the form
  • An admin login

We used an Enterprise Edition trial with these features on Summer '26 (API 67.0).

Your machine:

  • Salesforce CLI 2.152 or later (sf --version), Node.js and jq
  • Optional: qrencode (brew install qrencode), so scripts/link.sh can show the link as a QR code
  • scripts/link.sh uses the macOS date -j. On Linux, change its date line to GNU date -d.

For the test calls: a real mobile phone on cellular data, plus a laptop next to it.

Overview

Phase What Who Time we saw
0 Org, CLI login, recon HUMAN, then AI 15 min
1 Deploy the object, Apex and permission sets; run the tests AI 5 min
2 Guest site and form; phone test AI, then HUMAN 30 min
3 The agent; scripted preview of the whole loop AI 10 min
4 Phone number, routing flow, voice channel HUMAN, then AI checks 30 min, plus a few minutes for the number to go live
5 Live test call HUMAN + AI 10 min per call
6 Clean up Both 10 min

Phase 0: org and login

0.1 [HUMAN] Get an org that meets the prerequisites.

0.2 [HUMAN] Log the CLI in to it.

sf org login web -a typeback -r https://<your-domain>.my.salesforce.com

Approve in the browser straight away, because the CLI only waits a few minutes (AuthTimeoutError if you're slow). Use a browser window that's already logged in to the org. If you get a username and password page instead, paste that tab's URL into the window where you're logged in.

0.3 [AI] Create the local config.

cp .env.local.example .env.local      # then set ORG if you used an alias other than typeback

0.4 [AI] Match the API version. Check the org's API version, without printing the token:

sf org display -o typeback --json | jq -r '.result.apiVersion'

If it's lower than sourceApiVersion in sfdx-project.json (67.0), set sourceApiVersion to the org's version. Otherwise every retrieve fails with UNSUPPORTED_API_VERSION. A higher org version is fine.

0.5 [AI] Recon (read-only).

scripts/recon.sh

It shows the edition, licenses, existing agents, routing flows, calls, sites and credentials, and it copies existing agents and routing flows into org-reference/. Look for:

  • free Einstein Agent licenses
  • the Omni-Channel Phone service channel
  • whether Digital Experiences is on

Built-in routing flows show up by name but can't be copied. That's expected.

0.6 [AI] Check for a phone number entitlement.

sf data query -o typeback -q "SELECT MasterLabel, AmountUsed, CurrentAmountAllowed FROM TenantUsageEntitlement" --json |
  jq -r '.result.records[] | select(.MasterLabel | test("Long Code|Voice"; "i")) | "\(.MasterLabel): \(.AmountUsed // 0) of \(.CurrentAmountAllowed)"'

Ours showed one ten-digit long-code number and inbound voice credits. If you see nothing like that, [HUMAN] find out how your org gets a number before Phase 4.

Phase 1: the object, the Apex and the permission sets

1.1 [AI] Deploy and test.

scripts/setup.sh core

This deploys force-app, then runs the five Typeback test classes itself, because some orgs accept a RunSpecifiedTests deploy without running any tests. It also gives you Typeback_Admin.

Worked when: it prints 0 failed (we had 49 passing) and every Typeback class at 75% or more. Ours ranged from 86% to 100%.

Phase 2: the guest site and the form

2.1 [HUMAN] Turn on Digital Experiences, but only if recon said it's off. Setup → Digital Experiences → Settings → Enable Digital Experiences → pick a domain → Save. Treat the domain as permanent.

2.2 [AI] Create and publish the site.

scripts/setup.sh site

It creates an empty "Build Your Own (LWR)" site at /typeback and publishes it. It also gives the site's guest user Typeback_Guest. Site creation runs in the background and can take a few minutes. If publish fails because the site is brand new, wait a minute and re-run.

Worked when: it prints SITE_URL=https://<host>/typeback. Put that line in .env.local.

2.3 [AI] Write the settings.

scripts/configure.sh Log 10      # Log sender, links expire after 10 minutes, at most 3 links per call

2.4 [HUMAN] Activate the site, with its welcome email off. Setup → Digital Experiences → All Sites → Typeback → Workspaces → Administration:

  1. Under Emails, clear Send welcome email. Do this first: activating sends it to every site member.
  2. Under Settings, click Activate.

This is a person's step because it makes the site public and can email people.

[AI] check: sf data query -o typeback -q "SELECT Status FROM Network WHERE Name = 'Typeback'" shows Live.

2.5 [HUMAN] Place the form. All Sites → Typeback → Builder → Components (+) → Custom Components → drag Typeback Form onto the Home page → Publish.

2.6 [AI] Make a test link (nothing is texted):

scripts/new-link.sh

It prints a request name and a link. Each link works once and lasts 10 minutes. Links to one number are capped at 3 per 30 minutes, so for more, pass another fictional number: scripts/new-link.sh '415 555 0124'.

2.7 [HUMAN] Test the form on your phone, with Wi-Fi and any VPN off:

Test Expected
Open a link, type an email, Continue, Send "Thanks. Go back to your call and say done."
Open the same link again It says the link was already used
Type abc@ Blocked before it can be sent
(not run yet) scripts/configure.sh Log 1, make a link, wait 70 s, open it It says the link expired. Then run scripts/configure.sh Log 10

If the form says it can't load, the guest user can't run the controller. [HUMAN] In Builder → Settings → General → the Guest User Profile link → Enabled Apex Class Access → Edit → add TypebackFormController → Save.

After any later change to the form: [AI] run scripts/setup.sh core, then scripts/setup.sh site. The site serves its last publish, so the change doesn't show until you republish.

Phase 3: the agent

3.1 [AI] Set up, publish and activate the agent.

scripts/setup.sh agent

In order, it:

  1. Creates an Einstein Agent user, or reuses the one already in the .agent file, and writes it into default_agent_user.
  2. Gives that user Typeback_Agent.
  3. Validates, publishes and activates the newest version of TypebackDemoVoice.

Worked when: it prints TypebackDemoVoice version N is active.

Then put AGENT_USER=<the printed username> in .env.local, so later runs reuse that user instead of using up another license.

Run this before any preview. Without Typeback_Agent, the actions are withheld, and the agent improvises instead of saying the Apex lines.

3.2 [AI] Run the whole loop in a preview (live actions, Log sender, nothing texted):

scripts/preview-loop.sh

It plays a caller:

  1. Asks for an email, then gives a fictional number.
  2. Says "done" too early and asks a side question.
  3. Submits the form through the same controller the page calls.
  4. Says "done", confirms, and asks for a second email.

A second session backs out at the number prompt. The script checks each reply and the request row, and checks that the full email never appears in the preview traces.

Worked when: the last line is PASS. Each preview turn takes 3 to 5 seconds.

A preview has no call, so the agent asks for a mobile number. On a real call it uses caller ID instead.

How the agent works, if you change it:

  • A router guard runs first on every turn. It clears tb_line first. Then, if a link is out (tb_waiting), it runs the check action and goes straight to email_capture, with no LLM routing pass.
  • email_capture makes the link on entry, without the LLM, then says tb_line word for word.
  • Its tools are gated with available when on tb_status: send_to_number, resend_link, skip_email and confirm_email.
  • The resort facts live in the system instructions (check-in, spa hours and so on). They're made up.

Phase 4: the phone number and routing

The agent must be active (Phase 3) before you start, so it shows up in the routing picker. We did all of this in Setup. Deploying the flow as metadata wasn't tried.

4.1 [HUMAN] Get a number. Setup → Agentforce Voice Setup → Voice Channels with Agents → New Number. A wizard opens in a new window:

  1. Pick a country.
  2. Number type: 10-Digit Long Code.
  3. Pick a number.
  4. Review & Save → Finish.

The number shows as Requested, then Live within minutes. Use the Phone Numbers tab's Refresh Status to check. Ours was free because of the org's entitlement. If your org has none, check with whoever owns the org's contract before you go ahead.

4.2 [HUMAN] Build the routing flow. Setup → Flows → New Flow → Omni-Channel Flow.

  1. The flow needs a Text variable named recordId, available for input. Salesforce passes the VoiceCall Id in it. Create it if the flow doesn't have one.
  2. Add one Route Work element:
    • Route a single record: recordId
    • Service Channel: Phone
    • Route To: Agentforce Service Agent → Typeback Demo Voice
    • Fallback queue: any queue (we used the contact center's default queue)
  3. Save it as Typeback Voice Routing, then Activate.

The built-in voice routing templates are managed and can't be retrieved to copy, so build this one from scratch. It's only one element.

4.3 [HUMAN] Tie the number to the flow. Setup → Agentforce Voice Setup → Voice Channels with Agents → New Channel. Choose PSTN Voice, your number and Typeback Voice Routing → Save.

The channel is created inactive. Open the channel by clicking its name, then click Activate. The channel list has no toggle.

4.4 [AI] Check the routing.

sf data query -o typeback -q "SELECT ApiName, IsActive FROM FlowDefinitionView WHERE ApiName = 'Typeback_Voice_Routing'"
sf data query -o typeback -q "SELECT DeveloperName, MessageType, IsActive FROM MessagingChannel"

Worked when: the flow shows IsActive true, and there's a PstnVoice channel with IsActive true.

Optionally, retrieve the flow read-only to check it points at the agent. The output folder must be inside the project, and org-reference/ is gitignored.

sf project retrieve start -o typeback -m Flow:Typeback_Voice_Routing --target-metadata-dir org-reference/typeback-flow --unzip --json | jq -r '.status'

In the retrieved flow's routeWork action, "Agentforce Service Agent" is saved as:

Parameter Value
routingType Copilot
copilotId setupReference TypebackDemoVoice, type BotDefinition
botId empty
serviceChannelDevName sfdc_phone

Phase 5: a live test call

5.1 [AI] Start the link watcher in its own terminal, just before the call:

scripts/link.sh 300     # waits up to 5 minutes for the agent to make a link on a live call

It watches for a call in progress. As soon as the agent makes a link on it, the script prints the link, its expiry time and, if qrencode is installed, a QR code. This stands in for the text message.

5.2 [HUMAN] Make the call. Turn Wi-Fi off and use speakerphone, so you can type while you talk. Then call the number from your mobile. The first ring can take a while: that's before the call reaches Salesforce, which accepted ours 4 to 5 seconds after it arrived.

You The agent
"Can you email me my booking confirmation?" "I just texted a link to your phone ending in …. Open it, type your email, and say done when you've sent it."
(scan the QR code or open the printed link on your phone, type a test email, Continue, Send) (silent)
"Done." "Got it, thanks. I have an email starting with …, at … dot com. Is that right?"
"Yes." Thanks you and asks if there's anything else
"That's all, thanks." Wraps up. Hang up when it has

Note the time you called, so the call can be matched to its record.

5.3 [AI] Check the call.

scripts/report.sh 3

It lists the newest requests and calls, with phone numbers cut to their last four digits. It never reads the typed value. For the timings:

sf data query -o typeback -q "SELECT Id, CreatedDate, CallAcceptDateTime, CallEndDateTime, CallDurationInSeconds, CallDisposition, VendorType FROM VoiceCall ORDER BY CreatedDate DESC LIMIT 1"
sf data query -o typeback -q "SELECT Status, ServiceChannel.DeveloperName, CreatedDate, AcceptDateTime FROM AgentWork WHERE WorkItemId = '<VoiceCall Id>'"
sf data query -o typeback -q "SELECT Name, Status__c, CreatedDate, Submitted_At__c, Retrieved_At__c FROM Typeback_Request__c WHERE Voice_Call_Id__c = '<VoiceCall Id>'"

Passed when all of these hold:

  • The call completed with no hang-up and no transfer.
  • VendorType is VirtualVoiceAgent and the AgentWork is on sfdc_phone.
  • The call's FromPhoneNumber ends in your mobile's last four digits.
  • There's one request row with that Voice_Call_Id__c, its status is Submitted, and Retrieved_At__c is set.
  • The agent never said the full email.

Our result (the second live call):

From the call record's start
Agent accepts the call 5 s
Link made (after the greeting and the ask) 21 s
Form sent (link to the phone by hand, then typing) 59 s
Agent picks up the hint 62 s, 3 s after the form was sent
Call ends 84 s

Phase 6: clean up

  • [AI] Before any commit, put the agent user placeholder back: node scripts/setup-agent-user.js --username __AGENT_USER_PLACEHOLDER__
  • [HUMAN] The egress check's remote site, only if you ran the Twilio egress check: Setup → Remote Site Settings → Typeback_Egress_Check → Del. Don't use sf project delete source for this: it deletes the repo's copy too.
  • [HUMAN] To stop calls reaching the agent: deactivate the voice channel, or the Typeback Voice Routing flow. We found no way to release the number in Setup: the Phone Numbers tab only offers Refresh Status.
  • [AI] To turn the agent off: sf agent deactivate -o typeback -n TypebackDemoVoice, after the person says so.
  • [HUMAN] The agent user: deactivate it in Setup → Users if you're done with it.

Trial orgs expire. Nothing here depends on the org's history, so in a new org you can start again from Phase 0.

SMS: why the link is handed over by hand

The Log sender saves each link on its request row (Test_Link__c) instead of texting it. We looked at two ways to send real texts. Neither is built.

Native SMS on the same number: blocked in our org. The number was listed as SMS-capable in the wizard, but SMS is a separate messaging channel. Its steps, from Salesforce Help ("Create an SMS Channel in Agentforce Contact Center"):

  1. [HUMAN] Register a 10DLC brand and campaign: Setup → Messaging → Regulatory Compliance. This is required for US 10-digit long codes, and carriers can block unregistered traffic. It submits real company details, so only do it for a company you can register.
  2. [HUMAN] Setup → Communication Channels → New SMS Code. Pick the same country, Code Type 10-Digit Long Code, then the brand and campaign, then Port Agentforce Contact Center Voice and SMS Numbers. This needs the Agentforce Contact Center Admin (Salesforce Voice) permission set.
  3. [HUMAN] Download, fill in and upload the Long Code CSV and the Letter of Authorization, then Submit. Help says it takes up to one business day. The status is on the SMS Channel Requests tab.
  4. [HUMAN] All Channels → the new SMS channel → Activate Channel.

Where we stopped: at step 1, Register Brand said "You need an active brand license to register a brand. Contact your account executive for help." Nothing in the org gets round that. If your org has the license, the sending side would be the Send Conversation Messages action. It sends to messaging end-user records, which have a channel, a phone number and a consent status, and each send is tracked in ConvMessageSendRequest. A TypebackSms.Sender for it isn't written.

Twilio: not built. What exists: scripts/twilio-smoke.sh (one test SMS from your own terminal) and scripts/setup.sh egress. The egress check passed: the org reached Twilio's API and got the expected HTTP 401. Limits from Twilio's public docs:

  • A trial only texts verified numbers, and it adds a prefix to every message.
  • Toll-free numbers need verification before they can text US numbers.
  • US 10-digit numbers need A2P 10DLC registration.

To try it anyway:

  1. [AI] Egress check: scripts/setup.sh egress. Worked when: it prints PASS (HTTP 401, as expected without credentials). "Unauthorized endpoint" means the remote site setting is missing, so re-run. Any other error means the org can't reach Twilio.
  2. [HUMAN] Twilio account: sign up at twilio.com and verify your mobile. Copy the Account SID (AC…) from Console → Account Info. Buy an SMS-capable number. Then API keys & tokens → Create API key (Standard), and copy the key SID (SK…) and the secret, which is shown only once. Use an API key, not the account's Auth Token, so you can revoke the key on its own.
  3. [HUMAN] Fill in .env.local: TWILIO_ACCOUNT_SID, TWILIO_API_KEY_SID, TWILIO_FROM and TEST_MOBILE, in E.164 (+1…). The secret doesn't go there.
  4. [HUMAN] Smoke test: run scripts/twilio-smoke.sh in your own terminal and paste the secret at its hidden prompt. Worked when: the text arrives and its link opens the site. Otherwise the error code it prints says why: 21608 means a trial can't text that number because it isn't verified, and 30032 means the toll-free number isn't verified yet.

To add a sender:

  1. Implement TypebackSms.Sender and return it from TypebackSms.sender(name).
  2. Let scripts/configure.sh accept its name.
  3. Keep the callout before any DML, and put credentials in a named credential, never in code or settings.

Problems we hit, and the fixes

"In the repo" means the fix is already in the scripts or code, so you only hit it if you change them.

CLI and project

What happened Why Fix
AuthTimeoutError on sf org login web The CLI only waits a few minutes for you to click Allow Approve straight away, in a browser window already logged in to the org
Every retrieve failed with UNSUPPORTED_API_VERSION: Unknown version:68.0 sourceApiVersion was newer than the org Set it to the org's API version (Phase 0.4). In the repo: 67.0
Entity of type 'Flow' named 'VoiceRouting' cannot be found Built-in routing flows are listed but can't be retrieved. The managed voice routing templates can't be either Build the routing flow in Flow Builder (Phase 4.2)
A script log grew to 36 KB for one retrieve Without a terminal, sf project retrieve start prints every frame of its progress display Pass --json. In the repo
The output directory must be inside the current project Retrieves only write inside the SFDX project Retrieve into org-reference/, which is gitignored
CannotFindBundle: Cannot find an authoring bundle in …/force-app sf agent publish authoring-bundle only looks in the default package directory, and has no flag to point elsewhere Make force-app-agent the default package directory. The Apex deploy names -d force-app explicitly. In the repo

The guest site

What happened Why Fix
"Your Site is being created", then nothing LWR sites reject AuthenticationType=UNAUTHENTICATED, and the error only appears on the background job Use AUTHENTICATED_WITH_PUBLIC_ACCESS_ENABLED, which still lets guests in. In the repo
The script waited 10 minutes for a site that had already failed Site creation is asynchronous Check SELECT Status, Error FROM BackgroundOperation WHERE Id = '<jobId>'. In the repo
Installing plugin community… The first sf community command installs its plugin Nothing to do
The form opened without its token Publish returns the site's <prefix>vforcesite address, and its redirect drops the query string Links use https://<host>/typeback/?t=<token>. In the repo
Activating would email every site member Send welcome email is on by default Turn it off first, then activate (Phase 2.4)
A form change didn't show The site serves its last publish scripts/setup.sh site after every form deploy
The form says it can't load The guest user can't run the controller Add TypebackFormController to the guest profile's Apex class access (Phase 2.7)

Apex and tests

What happened Why Fix
Method does not exist or incorrect signature: void matcher(String) from the type String Apex names are case-insensitive, so a TOKEN constant and a token parameter clashed with no duplicate error Give constants distinct names (TOKEN_PATTERN). In the repo
Deploy said success, with Running Tests - Skipped and 0 tests This org accepted a RunSpecifiedTests deploy without running the tests Run sf apex run test … -c after the deploy. In the repo (setup.sh core)
Operation failed due to fields being inaccessible on Sobject Typeback_Settings__c in tests Tests updated the record that getOrgDefaults() returns Update a new record holding only the Id and the changed field. In the repo
A test comparing expiry to Datetime.now() failed now and then Saved datetimes lose their milliseconds Allow a second either side. In the repo
node --check fails on the LWC It can't parse decorators such as @api Expected. The org compiles the component on deploy

The agent

What happened Why Fix
The agent paraphrased ("Would you like to continue?") instead of saying the Apex line The agent user didn't have Typeback_Agent yet, so the actions were withheld with NO_USER_ACCESS (visible in the preview trace) Assign it before the first preview. In the repo (setup.sh agent)
An invented "3 PM" check-in, then a fallback to repeating the link line The model makes up facts, and the grounding check catches it Put the facts in the system instructions. In the repo
A retry within the turn said an old line tb_line was cleared in after_reasoning, so a retry saw it empty Clear it at the start of the router. In the repo
The agent answered a side question again on the next turn "Answer their question first" also covered earlier messages "If the caller's latest message also asks something…". In the repo
"Never mind, skip it" got "No problem", but the link stayed open The model agreed without calling the tool "You MUST call skip_email". In the repo
Agent user licenses ran short across rebuilds Each new agent user takes one Pin AGENT_USER in .env.local. The script reuses it
The preview asked for a mobile number A preview has no call, so there's no caller ID Expected. On a real call it uses caller ID

What we confirmed works in Agent Script on Summer '26, since the docs didn't say:

  • start_agent can have an actions: block.
  • Inside instructions:->, run @actions.X with … set … followed by a bare transition to works.
  • available when accepts and, not and !=.
  • True and False literals work in bindings, and boolean Apex outputs map to booleans.
  • The VoiceCallId: linked string variable with source: @VoiceCall.Id compiles, and it's filled in on a real phone call.
  • Preview with --use-live-actions runs the actions as the agent user.

Phone number, routing and calls

What happened Why Fix
The first call didn't reach the agent New voice channels are created inactive Open the channel's own page and click Activate. The list has no toggle
The flow's metadata says Copilot, with botId empty That's how "Agentforce Service Agent" is saved Expected. copilotId points at the agent's BotDefinition
The call took a while to connect It's carrier time, before the call reaches Salesforce Nothing to fix. Salesforce accepted the call 4 to 5 s after it arrived
No transcript on the call record (ConversationEntry empty) Live calls don't save one there Not solved. Agentforce session tracing could show the agent's side, but it wasn't turned on
scripts/link.sh fails on Linux It uses the macOS date -j Change its date line to GNU date -d

SMS

What happened Why Fix
The number's only action is Refresh Status SMS on a voice number is a separate messaging channel Setup → Communication Channels → New SMS Code (SMS)
Register Brand: "You need an active brand license to register a brand." The org has no brand license None in the org. The link is handed over by hand

Not done yet

  • Caller ID withheld. If the caller hides their number (*67 in the US), the agent should ask for a mobile number. That works in preview but hasn't been tried on a live call.
  • Edge cases on a live call: saying "done" too early, a side question, resends up to the cap, a long silence while typing. The first two pass in preview and the cap passes in the Apex tests. Silence hasn't been tried.
  • A baseline: timing the same request with the email spelled out, to compare against Typeback.
  • The expiry test on the form (Phase 2.7, last row).
  • Automatic SMS: see SMS.
  • A transcript of the agent's side of a live call.