Help & Support
Elevate Your Agents with AgentExchange Solutions
Have Questions?
Service Email To Case Configure
- assets/
- examples/
- references/
- scripts/
- SKILL.md
Configure Email-to-Case (Metadata API)
Configure Salesforce Email-to-Case entirely through the Metadata API (no Setup UI), for headless org configuration. The skill builds a CaseSettings source file for the desired emailToCase state, then applies it with scripts/apply-casesettings.py, which runs updateMetadata in two phases — the full emailToCase block + Support Settings (Phase A), then routing addresses (Phase B) — and verifies by re-reading. Two phases because a single deploy fails on a fresh org: On-Demand must be live before an address binds (see Rules / Constraints).
Scope
- In scope: Enabling Email-to-Case org-wide and setting its toggles (full field lists in Required Inputs and step 4); creating
EmailToCase,Outlook, andGmailOAuthrouting addresses; applying the change to an sf-CLI-authenticated org viaapply-casesettings.py; verifying by re-reading. - Out of scope:
E2cEasyrouting addresses — created only through the Service Easy Setup wizard, not the Metadata API; route the user there.- Authenticating the org — the user authenticates out-of-band (e.g.
sf org login web); the script reuses that session and never handles passwords or tokens. - Case object schema, layouts, assignment rules, or Web-to-Case.
Attaching an Agentforce service agent (delegation)
When the user wants an Agentforce Service Agent to answer these emails, configure the Email-to-Case base (the Workflow below), then delegate — this skill never creates or modifies agents.
Gate on org capability first. Attaching an agent needs the org entitled for Agentforce Email-to-Case; that entitlement surfaces as the BotEmailDefinition metadata type, so probe read-only before delegating:
scripts/check-agent-email-capability.sh <target-org-alias> # pins describe to the org's API version
Exit 3 → not entitled: stop, tell the user, and configure the plain Email-to-Case base only — make no Agentforce agent or channel-wiring changes. Any other non-zero is an unreachable org, not "not entitled" — fix auth and re-run. On exit 0, delegate:
- Agent creation →
agentforce-generate, omitting the Service Customer Verification topic (unsupported on the email channel) and including an Escalation subagent so the agent can hand off to a human — on email, escalation transfers the case to a service rep, and without the subagent the agent can't escalate. - Wiring →
service-agentforce-channel-configureBranch C.
Required Inputs
Gather or infer before applying. Ask only when a wrong assumption would produce a broken or incorrect result:
- Target org alias — the sf CLI alias/username of an already-authenticated org. The script obtains the session with
sf org displayandsf org auth show-access-token; it does not log in for you. - Routing address(es) — for each:
routingName(unique label),addressType(EmailToCasedefault, orOutlook/GmailOAuth),caseOrigin, andcasePriority. The platform rejects the address ifcaseOriginorcasePriorityis missing, so treat all four as required in the source file. - Customer-facing email address per routing address — ask explicitly for each, in address order; never guess, default, or reuse from earlier context. Not stored in the source file — passed at apply time via
--routing-email(one per address; the script fails closed on a count mismatch). - Support Settings — Default Case Owner & Automated Case User — preserve-first, never assumed:
- Independent, preserved per field. Each configured field is left untouched; change one only if the user asks (
--overwrite-support-settings). Ask only for the field(s) unset. - For each unset field, ask (never default to the authenticated user): Default Case Owner type (
User/Queue) + value (active Username or Queue DeveloperName) →--owner-type/--owner-value; Automated Case User type (User/System) —Userneeds a username,Systemneeds no value →--automated-type/--automated-value(+ optional--system-user-emailfor System when the org's automated user doesn't exist yet). - If any value is invalid, the script fails — get a valid value and re-run; never substitute or guess.
- Pass
--use-authenticated-useronly if the user explicitly asks.
- Independent, preserved per field. Each configured field is left untouched; change one only if the user asks (
- Case defaults per address (optional) —
createTask,saveEmailHeaders, and an optional per-address Default Case Owner (caseOwner+caseOwnerType), opt-in per address (see step 6): omitted by default so cases fall to the org Default Case Owner / assignment rules. When set,caseOwner(active Username or Queue DeveloperName) must be paired withcaseOwnerType(User/Queue); the script validates it against the org, failing closed if absent. - Org-level toggles (optional) — set only if the user asks. Two containers: the
emailToCasesection (enableHtmlEmail,notifyOwnerOnNewCaseEmail,enableE2CDeduplicateAttachments,showWordCountInComposer, plusenableE2CSourceTracking,overEmailLimitAction,unauthorizedSenderAction), and top-level Support Settings (enableDraftEmails— plural element name, a sibling ofemailToCase). See step 4's table for container/UI-label mapping; top-level scalar toggles apply in Phase A.
Defaults: enableEmailToCase and enableOnDemandEmailToCase default to true (both required — On-Demand before routing addresses); addressType defaults to EmailToCase. Support Settings have no default — preserved if set, otherwise elicited (never the authenticated user).
Workflow
All steps are sequential.
Confirm the org (and its type) — confirm the target org alias is authenticated (
sf org display --target-org <alias>). The apply script checks theOrganizationrow and refuses to mutate a production org (non-sandbox, non-trial) unless--confirm-productionis passed — enabling Email-to-Case is permanent and org-wide. If production, tell the user and confirm before re-running with--confirm-production. Sandboxes, scratch orgs, and trials deploy without the flag.Elicit the routing email(s) — ask the user for the exact customer-facing email address for each routing address (never assume, default, or carry a value over from earlier). Pass them to the script via
--routing-emailin the order the addresses appear in the source file.Load the template — read
assets/CaseSettings.settings-meta.xmlfor structure and field placement. Its comments are authoring guidance for you — never copy them into the output. Emit only the elements you set; the output must be comment-free, matchingexamples/CaseSettings-two-addresses.settings-meta.xml.Enable the E2C switches, then confirm the optional toggles — set both
enableEmailToCaseandenableOnDemandEmailToCasetotrue(both required; On-Demand must be on before routing addresses can be created — the script enables it in Phase A before Phase B). Then handle the non-required convenience settings, which the template turns on by default:UI label Field Container Enable HTML email enableHtmlEmailemailToCaseEliminate duplicate email attachments enableE2CDeduplicateAttachmentsemailToCaseShow word count in composer showWordCountInComposeremailToCaseNotify case owners on new emails notifyOwnerOnNewCaseEmailemailToCaseEnable Email Drafts enableDraftEmails(top-levelCaseSettings)Support Settings Before enabling these, tell the user exactly which will be turned on and let them opt out (e.g. "I'll also enable these optional settings: … — let me know if you'd like to skip any."). Remove any the user opts out of; keep the rest in their correct container (four inside
<emailToCase>,enableDraftEmailsat the root). Never silently enable these or any other optional flag the user didn't agree to. Confirmed toggles apply in Phase A.Pick the correct surface per routing address — for each address, decide by
addressTypeusingreferences/routing_address_reference.md. If any requested address isE2cEasy, stop and route the user to the Service Easy Setup wizard instead of emitting Metadata for it.Populate routing-address fields — for each
EmailToCase/Outlook/GmailOAuthaddress, setaddressType,routingName,caseOrigin, andcasePriority(all required — the platform rejects the operation with "Missing caseOrigin"/"Missing casePriority" if either is absent), plus any optional case defaults. Do not putemailAddressin the source file — it is supplied at apply time via--routing-email. Do not set the read-only fieldsemailServicesAddressorisVerified— the platform manages them. Seeexamples/CaseSettings-two-addresses.settings-meta.xmlfor a complete multi-address example.Per-address Default Case Owner (opt-in). Ask one address at a time whether cases from this address go to a specific owner or fall to the org Default Case Owner / assignment rules (the default) — different addresses can route to different owners. If the user declines, leave
caseOwner/caseOwnerTypeout of the source file entirely (the template ships them commented out). If the user opts in, ask for the type (User/Queue) and value (active Username or Queue DeveloperName) and set both<caseOwnerType>and<caseOwner>— the platform rejectscaseOwnerwithout its type.apply-casesettings.pyvalidates it against the org and fails closed if absent. Do not guess or reuse the top-level Default Case Owner.Determine Support Settings (Default Case Owner + Automated Case User) — run
python3 scripts/apply-casesettings.py --target-org <alias> --input <source-file> --verify-onlyand inspectdefaultCaseOwner/defaultCaseUser/useSystemUserAsDefaultCaseUser. When the user names a value, write it into the source file (defaultCaseOwner+defaultCaseOwnerType;defaultCaseUser, oruseSystemUserAsDefaultCaseUser+ optionalsystemUserEmail) — the scored, versionable artifact; the script also accepts these as flags. Leave each field the org already has as-is — omit it and it is preserved (override only if the user asks — then--overwrite-support-settings). For a field unset and unnamed, ask; never assume the authenticated user (--use-authenticated-useronly if explicitly asked).Validate the source file — run
python3 scripts/validate-casesettings.py <path-to-source-file>and resolve any reported errors before applying.Apply to the org — run:
python3 scripts/apply-casesettings.py --target-org <alias> --input <source-file> \ --routing-email <email-for-address-1> [--routing-email <email-for-address-2> ...] \ [--owner-type User|Queue --owner-value <username-or-queue-devname>] \ [--automated-type User --automated-value <username> | --automated-type System [--system-user-email <email>]] \ [--confirm-production]Provide exactly one
--routing-emailper routing address, in document order. Supply the owner/automated flags only when the org's Support Settings are not already configured (step 7); if configured, omit them and they are preserved. If the script rejects a value as invalid, relay it, get a valid value, and re-run — never fall back to the authenticated user. If the script reports production and exits, do not add--confirm-productionyourself — confirm with the user first (step 1). The script applies Phase A (support settings + toggles), then Phase B (routing addresses), re-reads to verify, and exits non-zero on any fault.Confirm the result — check the JSON summary:
verified.enableEmailToCase/verified.enableOnDemandEmailToCasearetrue,verified.routingAddressCountmatches the address count, eachphaseBentry isstatus: created(oralready_exists), andsupportSettings.actionispreserved-existing/set-from-input/set-from-authenticated-user. Use--verify-onlyto re-read without writing.
Configuration is complete after step 10.
After setup — post-completion guidance
The items below are not workflow steps — present them to the user once setup is confirmed.
Forwarding (required for real inbound mail)
Always give the user this guidance — it is how inbound mail to the customer-facing address actually reaches Salesforce:
Your routing address now has a Salesforce-generated email services address. To receive inbound mail, configure your email system to forward mail sent to your customer-facing address (e.g.
support@company.com) to that generated services address.
End-to-end proof (Act 3)
Offer to prove inbound email creates Cases — a live round-trip; skip it if the user declines. The skill never sends email — it orchestrates two human actions, then verifies with SOQL: (1) Address ownership — Salesforce emails a confirmation link; isVerified flips only when the human clicks it. Tell the user to click it and wait. (2) Send a test email — ask the user to send one real email from an external mailbox to the routing address's Salesforce-generated email services address (not the customer-facing address, which only receives mail once forwarding is configured), report the sender, and wait. (3) Verify with evidence — then run:
python3 scripts/apply-casesettings.py --target-org <alias> --verify-cases \
[--supplied-email <external-sender-address>]
Read-only: it queries Case where Origin='Email' (last 3 days, optionally narrowed by sender) plus the linked incoming EmailMessage rows (Incoming=true, ParentId = the new Case), and exits non-zero if no matching Case has a linked incoming email (mail not processed yet — wait and re-run — or address not verified). On success it prints the Case + EmailMessage evidence with proven: true.
Rules / Constraints
| Constraint | Rationale |
|---|---|
Apply via apply-casesettings.py (two-phase updateMetadata), never a plain deploy or a minimal emailToCase patch |
A single deploy fails on a fresh org; the full emailToCase block (not a field-level flip) is what provisions On-Demand before addresses bind. See references/apply-mechanics.md. |
Write only the top-level fields the skill owns; never echo the whole CaseSettings record |
The platform re-validates every top-level field in the payload (e.g. Case Feed → Chatter); sending only emailToCase + enableDraftEmails + Support Settings avoids unrelated failures and needs no Chatter prerequisite. Omitted fields keep their values via field-level merge. |
The customer-facing email must be explicit user input (--routing-email), never in the source file and never assumed |
Prevents a guessed or stale address from being provisioned. The script fails closed if --routing-email count ≠ address count. |
CaseSettings fullName is always Case |
It is an org singleton settings type; the script targets Case automatically. |
| Support Settings are preserved per field if configured; never assumed if not | Default Case Owner and Automated Case User are independent — each configured field is left untouched (unless --overwrite-support-settings), and only an unset field requires an explicit, org-validated value (fails closed otherwise). The authenticated user is used only with --use-authenticated-user. |
Automated Case User "System" and a named defaultCaseUser are mutually exclusive |
--automated-type System sets useSystemUserAsDefaultCaseUser and must not carry a user value; the platform rejects both. |
| The script reads → patches → updates (append-only for addresses) | It merges onto current settings, so existing routing addresses are preserved; a duplicate routingName/emailAddress is reported as already_exists. New addresses are emitted before existing ones (updateMetadata replaces the collection wholesale and would drop an existing address that a new one follows; a runtime guard fails the run if any existing address is dropped). See references/apply-mechanics.md. |
EmailToCaseSettings is never applied directly; enableEmailToCase cannot be turned off once on |
It is written only as the emailToCase child of CaseSettings. The platform permanently enables the feature after the first true apply; never rely on setting it back to false. |
Production orgs require explicit --confirm-production; never add the flag unprompted |
Enabling Email-to-Case is permanent and org-wide. The script fails closed on a non-sandbox, non-trial org (and one whose type it can't read) until the user confirms. Sandboxes and trials are exempt. |
| Proving inbound Cases is a read-only step the user triggers; the skill never sends email | --verify-cases only queries Case/EmailMessage after the human verified the address and sent a test email. Fails closed until a Case with a linked incoming email exists. |
E2cEasy addresses must use the Service Easy Setup wizard |
The prebuilt service_case queue binding and isVerified flip are not reproducible through the Metadata API. |
Never write emailServicesAddress or isVerified |
Both are read-only, platform-generated; the script strips them if present. |
| The generated source file contains only the elements being set — no XML comments | Template/example comments are authoring guidance, not output. Copying them bloats the file (gold is comment-free) and CLI-example comments with <placeholder> tokens can fail XML validation. Put explanation in your chat reply, never in the .settings-meta.xml. |
Gotchas
For the full symptom → resolution table (Phase B provisioning errors, Case Feed/Chatter, per-address owner half-fills, GmailOAuth conflicts, --verify-cases failures, and every toggle-placement pitfall), read references/troubleshooting.md.
Output Expectations
Deliverables:
- A
CaseSettingssource file (e.g.Case.settings-meta.xml) for the desiredemailToCasestate — toggles and routing-address fields, withoutemailAddress(supplied at apply time), read-only fields, or XML comments. Structure followsassets/CaseSettings.settings-meta.xml. - The applied org state from
apply-casesettings.py, plus its JSON summary (resolved owner, per-address status, verification read).
Reference File Index
| File | When to read / run |
|---|---|
assets/CaseSettings.settings-meta.xml |
Step 3 — the starting template for the source file |
references/apply-mechanics.md |
Read before touching how the script builds or sequences its updateMetadata payloads |
references/troubleshooting.md |
When the apply or verify step reports an error, or a configured setting doesn't behave — full symptom → resolution table |
references/routing_address_reference.md |
Steps 5–6 — routing-address field semantics and the addressType surface-selection rule |
examples/CaseSettings-two-addresses.settings-meta.xml |
Step 6 — to verify the source-file structure for multiple routing addresses |
scripts/check-agent-email-capability.sh |
Attaching an Agentforce service agent — the pre-delegation capability gate (probes for BotEmailDefinition) |
scripts/validate-casesettings.py |
Step 8 — deterministic structural validation before applying |
scripts/apply-casesettings.py |
Step 9 — applies the settings via two-phase updateMetadata and verifies |
scripts/tests/test_get_session.py |
Run when changing session-token acquisition (get_session / _usable_access_token) — the unit suite guarding token extraction and API-version resolution |
Related Skills
Salesforce
Build, modify, audit, repair, optimize, debug, and deploy agents with Agentforce Agent Script. TRIGGER when: user creates, reviews, or changes .agent files or aiAuthoringBundle metadata; asks to fix AgentScript, audit an existing agent, run an AgentScript
Categories
Agentforce Content
Salesforce
Wires an existing, active Agentforce agent to a channel by resolving a fallback queue, setting up inbound routing (either PATCH SessionHandlerId on the MessagingChannel, or an inbound RoutingFlow for Voice/Email), and optionally configuring outbound escal