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
source file for the desired
state, then applies it with
scripts/apply-casesettings.py
, which runs
in
two phases — the full
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 , , and routing addresses; applying the change to an sf-CLI-authenticated org via ; verifying by re-reading.
- Out of scope:
- routing 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. ); 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
metadata type, so probe read-only
before delegating:
bash
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 → , 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-configure
Branch 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 and
sf org auth show-access-token
; it does not log in for you.
- Routing address(es) — for each: (unique label), ( default, or / ), , and . The platform rejects the address if or is 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 (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 (/) + value (active Username or Queue DeveloperName) → /; Automated Case User type (/) — needs a username, needs no value → / (+ optional for 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 only if the user explicitly asks.
- Case defaults per address (optional) — , , and an optional per-address Default Case Owner ( + ), opt-in per address (see step 6): omitted by default so cases fall to the org Default Case Owner / assignment rules. When set, (active Username or Queue DeveloperName) must be paired with (/); the script validates it against the org, failing closed if absent.
- Org-level toggles (optional) — set only if the user asks. Two containers: the section (,
notifyOwnerOnNewCaseEmail
, enableE2CDeduplicateAttachments
, , plus , , ), and top-level Support Settings ( — plural element name, a sibling of ). See step 4's table for container/UI-label mapping; top-level scalar toggles apply in Phase A.
Defaults:
and
enableOnDemandEmailToCase
default to
(both required — On-Demand before routing addresses);
defaults to
. 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 the
row and
refuses to mutate a production org (non-sandbox, non-trial) unless
is passed — enabling Email-to-Case is permanent and org-wide. If production, tell the user and confirm before re-running with
. 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
in the order the addresses appear in the source file.
-
Load the template — read
assets/CaseSettings.settings-meta.xml
for 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, matching
examples/CaseSettings-two-addresses.settings-meta.xml
.
-
Enable the E2C switches, then confirm the optional toggles — set both
and
enableOnDemandEmailToCase
to
(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 | | |
| Eliminate duplicate email attachments | enableE2CDeduplicateAttachments
| |
| Show word count in composer | | |
| Notify case owners on new emails | notifyOwnerOnNewCaseEmail
| |
| Enable Email Drafts | (top-level ) | 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
,
at 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
using
references/routing_address_reference.md
. If any requested address is
, stop and route the user to the Service Easy Setup wizard instead of emitting Metadata for it.
-
Populate routing-address fields — for each
/
/
address, set
,
,
, and
(all required — the platform rejects the operation with "Missing caseOrigin"/"Missing casePriority" if either is absent), plus any optional case defaults. Do
not put
in the source file — it is supplied at apply time via
. Do
not set the read-only fields
or
— the platform manages them. See
examples/CaseSettings-two-addresses.settings-meta.xml
for 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
/
out of the source file entirely (the template ships them commented out). If the user
opts in, ask for the
type (
/
) and
value (active Username or Queue DeveloperName) and set
both and
— the platform rejects
without its type.
validates 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-only
and inspect
/
/
useSystemUserAsDefaultCaseUser
.
When the user names a value, write it into the source file (
+
;
, or
useSystemUserAsDefaultCaseUser
+ optional
) — 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 (
only 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:
bash
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
per 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 yourself — 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.enableOnDemandEmailToCase
are
,
verified.routingAddressCount
matches the address count, each
entry is
(or
), and
is
/
/
set-from-authenticated-user
. Use
to 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.
) 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;
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:
bash
python3 scripts/apply-casesettings.py --target-org <alias> --verify-cases \
[--supplied-email <external-sender-address>]
Read-only: it queries
where
(last 3 days, optionally narrowed by sender) plus the linked incoming
rows (
,
= 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
+
evidence with
.
Rules / Constraints
| Constraint | Rationale |
|---|
| Apply via (two-phase ), never a plain deploy or a minimal patch | A single deploy fails on a fresh org; the full 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 record | The platform re-validates every top-level field in the payload (e.g. Case Feed → Chatter); sending only + + 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 (), never in the source file and never assumed | Prevents a guessed or stale address from being provisioned. The script fails closed if count ≠ address count. |
| is always | It is an org singleton settings type; the script targets 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 . |
| Automated Case User "System" and a named are mutually exclusive | 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 / is reported as . New addresses are emitted before existing ones ( 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
. |
| is never applied directly; cannot be turned off once on | It is written only as the child of . The platform permanently enables the feature after the first apply; never rely on setting it back to . |
| Production orgs require explicit ; 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 | only queries / after the human verified the address and sent a test email. Fails closed until a Case with a linked incoming email exists. |
| addresses must use the Service Easy Setup wizard | The prebuilt queue binding and flip are not reproducible through the Metadata API. |
| Never write or | 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 tokens can fail XML validation. Put explanation in your chat reply, never in the . |
Gotchas
For the full symptom → resolution table (Phase B provisioning errors, Case Feed/Chatter, per-address owner half-fills,
conflicts,
failures, and every toggle-placement pitfall), read
references/troubleshooting.md
.
Output Expectations
Deliverables:
- A source file (e.g. ) for the desired state — toggles and routing-address fields, without (supplied at apply time), read-only fields, or XML comments. Structure follows
assets/CaseSettings.settings-meta.xml
.
- The applied org state from , 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 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 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 ) |
scripts/validate-casesettings.py
| Step 8 — deterministic structural validation before applying |
scripts/apply-casesettings.py
| Step 9 — applies the settings via two-phase and verifies |
scripts/tests/test_get_session.py
| Run when changing session-token acquisition ( / ) — the unit suite guarding token extraction and API-version resolution |