assets/confirmation-output.json
{
"type": "ThunderbirdVoiceSettings",
"fullName": "ThunderbirdVoice",
"apiVersion": "68.0",
"method": "sf project deploy (Settings/ThunderbirdVoice)",
"headless": true,
"preferences": [
{ "name": "enableCallRecording", "enabled": true },
{ "name": "enableCallTranscription", "enabled": true }
],
"preserved": [
{ "name": "enableCallRecRedaction", "enabled": true },
{ "name": "enableSipRecording", "enabled": false }
],
"status": "ENABLED"
}
assets/package.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
Metadata API manifest for retrieving/deploying ThunderbirdVoiceSettings via the
sf CLI Settings container. scripts/enable-recording-transcription.sh generates
this manifest into a temporary SFDX project (substituting the API version).
Key points:
- The type is the generic `Settings` container (which IS in the sf CLI
metadata registry) — NOT `ThunderbirdVoiceSettings` (which is not a
retrievable top-level type name).
- The member is the mdApiType WITHOUT the trailing `Settings`, i.e.
`ThunderbirdVoice`.
- version MUST be >= 68.0. ThunderbirdVoiceSettings fields are not exposed at
any lower version, so a retrieve at v64-67 returns the type empty/absent.
-->
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>ThunderbirdVoice</members>
<name>Settings</name>
</types>
<version>68.0</version>
</Package>
references/thunderbird-voice-settings.md
# ThunderbirdVoiceSettings — Metadata API contract
Reference for configuring — enabling **or disabling** — Native Voice (Thunderbird
Voice) recording and transcription via the Metadata API, using the sf CLI
`Settings`-container retrieve/deploy path. Read this when constructing or debugging
`scripts/enable-recording-transcription.sh`.
## The metadata type
`ThunderbirdVoiceSettings` is a singleton settings type (extends `Metadata`),
addressed by a fixed `fullName`:
- **type (mdApiType)**: `ThunderbirdVoiceSettings`
- **fullName**: `ThunderbirdVoice`
This skill manages exactly two boolean preferences:
| Field | Meaning | This skill |
|-------|---------|------------|
| `enableCallRecording` | Capture audio of Native Voice calls | **set to the requested state** (`true`/`false`) when named (target) |
| `enableCallTranscription` | Produce transcripts of Native Voice calls | **set to the requested state** (`true`/`false`) when named (target) |
Recording and transcription are independent — a call can be recorded without being
transcribed and vice versa — so they are always modeled and reported as two separate
preferences, and each can be enabled or disabled on its own. A target preference the
caller does not name is left at its retrieved value, exactly like a sibling.
The type may expose other booleans (e.g. `enableCallRecRedaction`,
`enableRealTimeStreamingRecording`, `enableSipRecording`). They are **out of scope** to
*change*: the skill reads them only to preserve their existing values on the deployed
file. It does, however, **echo them back unchanged** in a `preserved` block of the
confirmation JSON — reporting a sibling's retrieved value is not the same as setting it,
and compliance callers rely on that block for explicit "nothing else moved" assurance.
No sibling is ever reported as changed.
## Why the sf CLI, not raw SOAP
`ThunderbirdVoiceSettings` is not a retrievable top-level type name, so it is easy to
conclude "raw SOAP `updateMetadata()` is the only path." That is wrong. The generic
**`Settings`** container type *is* in the sf CLI metadata registry, and its member is
the mdApiType **without** the trailing `Settings` — i.e. `ThunderbirdVoice`. Retrieving
and deploying through that container:
- authenticates with the org's existing **`sf` login** — no browser session id /
access token is ever handled (raw SOAP on internal pc-rnd orgs rejects CLI OAuth
sessions on the SOAP endpoint, which is why a SOAP path there needs a browser sid);
- lets us **retrieve first** to check the gate and snapshot sibling prefs, then deploy
a full file that preserves them;
- gives a built-in **verify** step (round-trip retrieve).
## The manifest
`assets/package.xml` is the manifest template (the script generates it into a temp
SFDX project). Two things matter:
```xml
<types><members>ThunderbirdVoice</members><name>Settings</name></types>
<version>68.0</version>
```
- **member** = `ThunderbirdVoice` (mdApiType minus `Settings`), **name** = `Settings`.
- **Pin the version to 68.0.** `ThunderbirdVoiceSettings` fields are only exposed at
API **v68.0 and above**, so anything below 68.0 returns the type empty/absent — the
fields simply are not in the schema at those versions. But do NOT reach for the CLI's
newest default either: a retrieve/deploy at a version **above the org's max supported
API** SILENTLY returns an empty result (no error), indistinguishable from "feature not
provisioned". 68.0 is the field-exposure floor and works on every org that has the
feature. (Verified sweep on thb-einstein, org max v68.0: v64/62/60 → "Settings type
'ThunderbirdVoice' is unknown"; v65/66/67 → type absent; v68 → fields present, deploy
round-trips; **v69 → success:true but files:[] — silent empty**.)
## Sequence
1. **Retrieve** `-x package.xml` → `force-app/main/default/settings/ThunderbirdVoice.settings-meta.xml`.
2. **Gate check.** If the file is an empty self-closing `<ThunderbirdVoiceSettings/>`
(no fields), the org-perm gate `ThunderbirdVoice.orgHasNativeVoiceAllowed` is **off**
— Native Voice is not provisioned. Stop; this is enabled via provisioning/Blacktab,
not this skill. If the fields are listed, the gate is on — proceed.
3. **Set** each named target (`enableCallRecording` and/or `enableCallTranscription`)
to its requested value (`true` to enable, `false` to disable); leave every other
field — siblings and any un-named target — at its retrieved value.
4. **Validate** with `deploy --dry-run` (never skip).
5. **Deploy**.
6. **Verify** by re-retrieving and confirming the org *returns* each named field at
its requested value.
## Distinguishing the two failure modes
| Symptom | Cause | What to do |
|---------|-------|------------|
| Retrieve returns fields, deploy round-trips | Working as intended | done |
| Retrieve returns an **empty** `<ThunderbirdVoiceSettings/>` | Org-perm gate off (feature not provisioned) | provision / Blacktab — not fixable via metadata |
| Retrieve returns **no** settings file, or `Settings` type "unknown" | **API version too low** (< 68.0) | raise `--api-version` to 68.0+ |
> The historical `FIELD_INTEGRITY_EXCEPTION: Property 'enableCallRecording' not valid
> in version <N>` error was **a version problem, not a feature gate** — it appeared when
> the request used an API version below 68.0. Bumping to v68.0+ resolves it. Do not
> mistake it for an unprovisioned feature; distinguish the two with the retrieve gate
> check above.
## Auth
`scripts/enable-recording-transcription.sh --target-org <alias>` uses the sf CLI's
existing authentication for that alias (confirm with `sf org display -o <alias>`). No
session id or access token is requested, handled, or printed.
scripts/enable-recording-transcription.sh
#!/usr/bin/env bash
#
# Configure Native Voice (Thunderbird Voice) call recording + transcription via
# the Metadata API, using the sf CLI Settings-container retrieve/deploy path.
#
# Scope: this skill configures exactly two preferences — enableCallRecording and
# enableCallTranscription — to a requested ON or OFF state. Either or both may be
# targeted (`--recording on|off`, `--transcription on|off`); a preference that is
# NOT named is left at its retrieved value (preserved), so you can turn one on or
# off without disturbing the other. With NO preference flags the script defaults
# to turning BOTH on (the original enable-both behavior). Every OTHER
# ThunderbirdVoiceSettings boolean is read only to be preserved verbatim — none
# is changed. The siblings ARE reported back (unchanged) in a `preserved` block
# so compliance callers can confirm nothing else moved; reporting a value is not
# the same as setting it.
#
# Why sf CLI (not raw SOAP): the generic `Settings` metadata type IS in the sf
# CLI registry (member = the type name without the `Settings` suffix, i.e.
# `ThunderbirdVoice`). Deploying through it authenticates with the org's existing
# `sf` login — no browser session id / access token is ever handled. Retrieving
# first lets us (a) check the org-perm gate and (b) preserve non-targeted prefs.
#
# Sequence: retrieve -> gate check -> set the targeted field(s) to the requested
# state -> validate (dry-run) -> deploy -> verify by round-trip -> print a JSON
# confirmation reporting the resulting state of BOTH preferences.
#
# Deps: sf, jq. (No curl, no session id.)
#
set -euo pipefail
fail() { echo "ERROR: $*" >&2; exit 1; }
# ThunderbirdVoiceSettings fields are only exposed at API v68.0+; below that the
# Settings type does not appear in a retrieve at all. Default to exactly 68.0 — the
# field-exposure floor. A retrieve/deploy at a version ABOVE the org's max supported
# API SILENTLY returns an empty result (no error), which is indistinguishable from
# "feature not provisioned". 68.0 works on every org that has the feature; do NOT
# bump this default to a newer version unless you also cap it to the org's max.
API_VERSION="68.0"
TARGET_ORG=""
DRY_RUN="false"
# Desired state per target field: "" = not targeted (preserve), else "true"/"false".
REC_STATE=""
TRANS_STATE=""
# mdApiType ThunderbirdVoiceSettings -> package.xml member without the "Settings" suffix.
MEMBER="ThunderbirdVoice"
SETTINGS_FILE_REL="force-app/main/default/settings/${MEMBER}.settings-meta.xml"
usage() {
cat <<'EOF'
Configure Native Voice call recording + transcription (sf CLI Settings deploy).
Usage:
enable-recording-transcription.sh --target-org <alias> \
[--recording on|off] [--transcription on|off] [--dry-run] [--api-version X.Y]
Options:
--target-org <alias> Org alias/username the sf CLI is authenticated to. Required.
--recording on|off Desired state for call recording (enableCallRecording).
--transcription on|off Desired state for call transcription (enableCallTranscription).
If NEITHER --recording nor --transcription is given, the
script defaults to turning BOTH on. A preference that is
not named is left at its current (retrieved) value.
--api-version <X.Y> Metadata API version (default: 68.0; must be >= 68.0, the
version at which ThunderbirdVoiceSettings is exposed). Do NOT
exceed the target org's max supported version — a retrieve
above it silently returns empty.
--dry-run Retrieve + validate the change (deploy --dry-run) but do NOT
persist it. Prints the JSON confirmation with status VALIDATED.
-h, --help Show this help.
On success prints a JSON confirmation object: BOTH preferences reported separately
with their resulting state, plus a `preserved` block echoing every sibling boolean
(unchanged) so callers can confirm nothing else moved.
Uses the org's sf login; no session id / access token is handled or printed.
EOF
}
# Normalize an on/off style value to the literal "true"/"false" used in the XML.
onoff() { # $1 = flag name (for errors), $2 = value
case "$2" in
on|true|enable|enabled) echo "true" ;;
off|false|disable|disabled) echo "false" ;;
*) fail "invalid value for $1: '$2' (expected on|off)." ;;
esac
}
while [ $# -gt 0 ]; do
case "$1" in
--target-org) TARGET_ORG="$2"; shift 2 ;;
--api-version) API_VERSION="$2"; shift 2 ;;
--recording) REC_STATE="$(onoff --recording "$2")"; shift 2 ;;
--transcription) TRANS_STATE="$(onoff --transcription "$2")"; shift 2 ;;
--dry-run) DRY_RUN="true"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
# Default: if neither preference was named, target BOTH on — the original
# enable-both behavior, so existing callers keep working unchanged.
if [ -z "$REC_STATE" ] && [ -z "$TRANS_STATE" ]; then
REC_STATE="true"; TRANS_STATE="true"
fi
command -v sf >/dev/null 2>&1 || fail "sf CLI not found (required)."
command -v jq >/dev/null 2>&1 || fail "jq not found (required)."
[ -n "$TARGET_ORG" ] || fail "--target-org is required."
# Guard the version floor deterministically. Require a full X.Y form so a bare
# major like "68" can't end up as <version>68</version> in the manifest.
printf '%s' "$API_VERSION" | grep -qE '^[0-9]+\.[0-9]+$' \
|| fail "invalid --api-version '$API_VERSION' (expected X.Y, e.g. 68.0)."
API_MAJOR="${API_VERSION%%.*}"
[ "$API_MAJOR" -ge 68 ] || fail "--api-version must be >= 68.0 (ThunderbirdVoiceSettings is not exposed below v68.0). Got $API_VERSION."
# --- scaffold an isolated SFDX project so the retrieve/deploy is self-contained
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
sf project generate --name p --output-dir "$WORKDIR" >/dev/null 2>&1 \
|| fail "could not scaffold a temporary SFDX project."
PROJ="$WORKDIR/p"
cat > "$PROJ/package.xml" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types><members>${MEMBER}</members><name>Settings</name></types>
<version>${API_VERSION}</version>
</Package>
EOF
SETTINGS_FILE="$PROJ/$SETTINGS_FILE_REL"
# Read the literal true/false a boolean field currently holds in the settings
# file; echoes "false" if the field is absent (guarded so an empty grep under
# `set -o pipefail` does not abort the script).
read_pref() { # $1 = settings xml file, $2 = field name
local v
v="$({ grep -oE "<$2>(true|false)</$2>" "$1" || true; } | sed -E "s#<$2>([a-z]+)</$2>#\1#" | head -n1)"
echo "${v:-false}"
}
# --- step 1: retrieve current settings (gate check + sibling snapshot) --------
( cd "$PROJ" && sf project retrieve start -x package.xml -o "$TARGET_ORG" >/dev/null 2>&1 ) \
|| fail "retrieve failed for org '$TARGET_ORG'. Check the alias is connected (sf org display -o $TARGET_ORG)."
[ -f "$SETTINGS_FILE" ] || fail "retrieve returned no ${MEMBER}.settings-meta.xml — the Settings type was not returned at v${API_VERSION}. Two causes: (a) v${API_VERSION} is BELOW 68.0 (fields not yet exposed), or (b) v${API_VERSION} is ABOVE the org's max supported API version, which SILENTLY returns empty. Pin --api-version to the org's max (68.0 works on every org that has the feature)."
# Gate check: an empty self-closing element means the org-perm gate
# (orgHasNativeVoiceAllowed) is OFF — the feature is not provisioned. The fields
# are only present when the gate is on.
if ! grep -q '<enableCallRecording>' "$SETTINGS_FILE"; then
fail "Native Voice is not provisioned on '$TARGET_ORG' (ThunderbirdVoiceSettings returned no fields). This is an org-perm gate enabled through provisioning/Blacktab, not something this skill can flip."
fi
# --- step 2: set ONLY the targeted field(s) to the requested state ------------
# A field that is not targeted is left at its retrieved value (preserved), just
# like the siblings. Version-suffixed sed -i for BSD (macOS) + GNU portability.
SED_ARGS=()
[ -n "$REC_STATE" ] && SED_ARGS+=(-e "s|<enableCallRecording>[^<]*</enableCallRecording>|<enableCallRecording>${REC_STATE}</enableCallRecording>|")
[ -n "$TRANS_STATE" ] && SED_ARGS+=(-e "s|<enableCallTranscription>[^<]*</enableCallTranscription>|<enableCallTranscription>${TRANS_STATE}</enableCallTranscription>|")
sed -E -i.bak "${SED_ARGS[@]}" "$SETTINGS_FILE"
rm -f "${SETTINGS_FILE}.bak"
# A targeted field must now hold the requested value; if the tag was absent the
# sed matched nothing (a provisioned org exposes both, so this is an edge case).
[ -z "$REC_STATE" ] || grep -q "<enableCallRecording>${REC_STATE}</enableCallRecording>" "$SETTINGS_FILE" || fail "could not set enableCallRecording=${REC_STATE} in the retrieved file (field absent?)."
[ -z "$TRANS_STATE" ] || grep -q "<enableCallTranscription>${TRANS_STATE}</enableCallTranscription>" "$SETTINGS_FILE" || fail "could not set enableCallTranscription=${TRANS_STATE} in the retrieved file (field absent?)."
# --- step 3: validate (dry-run). Never skip. ---------------------------------
( cd "$PROJ" && sf project deploy start -x package.xml -o "$TARGET_ORG" --dry-run --ignore-conflicts >/dev/null 2>&1 ) \
|| fail "validation (deploy --dry-run) failed — the org rejected the change. Re-check the gate (step 1)."
# Collect every boolean sibling (<tag>true|false</tag>) EXCEPT the two target
# prefs from the settings file, as a JSON array [{name, enabled}], so the
# confirmation can attest each sibling was retrieved and carried through
# unchanged. `grep -o` isolates each pair; `#`-delimited sed avoids colliding
# with the `|` alternation; the `|| true` guards keep an empty result (no
# siblings) from tripping `set -o pipefail`.
extract_preserved() { # $1 = settings xml file -> JSON array on stdout
{ grep -oE '<[A-Za-z][A-Za-z0-9]*>(true|false)</[A-Za-z][A-Za-z0-9]*>' "$1" || true; } \
| sed -E 's#<([A-Za-z0-9]+)>([a-z]+)</[A-Za-z0-9]+>#\1 \2#' \
| { grep -vE '^(enableCallRecording|enableCallTranscription) ' || true; } \
| jq -R -s 'split("\n") | map(select(length > 0) | split(" ") | { name: .[0], enabled: (.[1] == "true") })'
}
# Overall status word from the resulting state of the two target preferences.
compute_status() { # $1 = recording bool, $2 = transcription bool
if [ "$1" = "true" ] && [ "$2" = "true" ]; then echo "ENABLED"
elif [ "$1" = "false" ] && [ "$2" = "false" ]; then echo "DISABLED"
else echo "CONFIGURED"; fi
}
emit_json() { # $1 = recording bool, $2 = transcription bool, $3 = status, $4 = preserved JSON
jq -n --arg apiVersion "$API_VERSION" --arg status "$3" \
--argjson recording "$1" --argjson transcription "$2" --argjson preserved "$4" \
'{
type: "ThunderbirdVoiceSettings",
fullName: "ThunderbirdVoice",
apiVersion: $apiVersion,
method: "sf project deploy (Settings/ThunderbirdVoice)",
headless: true,
preferences: [
{ name: "enableCallRecording", enabled: $recording },
{ name: "enableCallTranscription", enabled: $transcription }
],
preserved: $preserved,
status: $status
}'
}
if [ "$DRY_RUN" = "true" ]; then
# The local file already reflects the requested change (targets flipped, the
# rest at their retrieved values); report both prefs from it.
REC_FINAL="$(read_pref "$SETTINGS_FILE" enableCallRecording)"
TRANS_FINAL="$(read_pref "$SETTINGS_FILE" enableCallTranscription)"
emit_json "$REC_FINAL" "$TRANS_FINAL" "VALIDATED" "$(extract_preserved "$SETTINGS_FILE")"
exit 0
fi
# --- step 4: deploy ----------------------------------------------------------
( cd "$PROJ" && sf project deploy start -x package.xml -o "$TARGET_ORG" --ignore-conflicts >/dev/null 2>&1 ) \
|| fail "deploy failed after a successful dry-run."
# --- step 5: verify by round-trip (do not trust "Succeeded") -----------------
rm -f "$SETTINGS_FILE"
( cd "$PROJ" && sf project retrieve start -x package.xml -o "$TARGET_ORG" >/dev/null 2>&1 ) \
|| fail "post-deploy retrieve failed; cannot verify."
REC_FINAL="$(read_pref "$SETTINGS_FILE" enableCallRecording)"
TRANS_FINAL="$(read_pref "$SETTINGS_FILE" enableCallTranscription)"
# Each targeted field must come back at the requested state (untargeted fields
# are whatever the org holds — reported, not asserted).
[ -z "$REC_STATE" ] || [ "$REC_FINAL" = "$REC_STATE" ] || fail "verification failed: org did not return enableCallRecording=${REC_STATE} (got ${REC_FINAL})."
[ -z "$TRANS_STATE" ] || [ "$TRANS_FINAL" = "$TRANS_STATE" ] || fail "verification failed: org did not return enableCallTranscription=${TRANS_STATE} (got ${TRANS_FINAL})."
# Report siblings from the post-deploy round-tripped file — their real, current
# (unchanged) values, proving nothing else moved.
emit_json "$REC_FINAL" "$TRANS_FINAL" "$(compute_status "$REC_FINAL" "$TRANS_FINAL")" "$(extract_preserved "$SETTINGS_FILE")"
SKILL.md
---
name: service-native-voice-recording-transcription-configure
description: "Configure (enable OR disable) call recording and call transcription for Native Voice (Thunderbird Voice) programmatically via the Metadata API, for headless / API-driven support where no human uses the Service Console UI. Either preference can be turned on or off independently. Use this skill when a user asks to turn on/off, enable/disable, or configure Native Voice recording and/or transcription for headless or automated voice calls. TRIGGER when: users mention Thunderbird Voice, Native Voice, ThunderbirdVoiceSettings, enableCallRecording, enableCallTranscription, 'enable recording and transcription', 'turn off call recording', 'disable transcription', 'turn on call recording via API', headless voice recording, or configuring voice-call capture without clicking through Setup. DO NOT TRIGGER when: the user wants to redact or scrub PII from existing recordings, transcribe a supplied audio file, or author a Flow/Apex — those are out of scope for this skill."
metadata:
version: "1.2"
domains: ["Service"]
minApiVersion: "68.0"
accessCheck:
- type: "orgPerm"
value: "ThunderbirdVoice.orgHasNativeVoiceAllowed"
cliTools:
- tool: ["jq"]
semver: ">=1.6"
- tool: ["sf"]
semver: ">=2.0.0"
---
# Configure Native Voice Recording & Transcription (headless)
Turn call **recording** and call **transcription** for Native Voice (Thunderbird
Voice) **on or off** on an org, programmatically, so the change takes effect for
headless / API-driven support sessions that never touch the Service Console UI.
Recording and transcription are **two separate preferences** — this skill can set
either or both to the requested state (leaving the other untouched) and returns a
structured JSON confirmation.
## Scope
- **In scope**: setting `enableCallRecording` and/or `enableCallTranscription` to a
requested state — **`true` (enable) or `false` (disable)** — on the
`ThunderbirdVoiceSettings` metadata via an sf CLI Metadata API deploy (the generic
`Settings` container, member `ThunderbirdVoice`); returning a JSON confirmation of
the resulting configuration. A preference the user does not name is left at its
current value.
- **Out of scope**: changing any other `ThunderbirdVoiceSettings` boolean (e.g.
call-recording redaction, SIP recording) — siblings are read only to be preserved,
never set or reported; redacting/scrubbing PII from existing recordings or
transcripts; transcribing a supplied audio file; provisioning the Native Voice /
Thunderbird channel itself; authoring Flow, Apex, or permission sets. These are
outside this skill's scope — handle them separately.
---
## Required Inputs
Gather or infer before proceeding:
- **Target org** — an org alias or username the sf CLI is authenticated to. The deploy
uses this existing `sf` login; no session id or access token is requested. Default:
the org's default target. Confirm connectivity with `sf org display -o <alias>`.
- **Which preferences** — recording, transcription, or both. Default: **both**, since
headless capture typically needs recording *and* transcription. If the user named
just one, target only that one and leave the other at its current value.
- **Desired state** — enable (`on`/`true`) or disable (`off`/`false`) for each named
preference. Default: **enable**, unless the user asks to turn something off. Honor a
disable request literally — never re-enable a preference the user asked to turn off.
---
## Workflow
All steps are sequential. Do not skip or reorder. If blocked, stop and ask for the
missing org context.
1. **Confirm the two preferences are modeled separately.** Recording
(`enableCallRecording`) and transcription (`enableCallTranscription`) are distinct
booleans on the same metadata type — never collapse them into one combined flag.
The final confirmation must report each independently.
2. **Run the configure script.** Run
`scripts/enable-recording-transcription.sh --target-org <alias>` with the desired
state per preference: `--recording on|off` and/or `--transcription on|off`. With no
preference flags it defaults to turning **both on**. Examples: turn both off →
`--recording off --transcription off`; enable recording only →
`--recording on` (transcription left as-is); disable transcription only →
`--transcription off`. The script retrieves `ThunderbirdVoiceSettings` (via the
`Settings` container), checks the org-perm gate, sets **only the named** flag(s) to
the requested value while preserving every other field, validates with a dry-run
deploy, deploys, and verifies by round-trip. Use `--dry-run` to retrieve + validate
without persisting. Run `scripts/enable-recording-transcription.sh --help` for all
options.
3. **For the Metadata API contract**, read `references/thunderbird-voice-settings.md` —
it documents the type, the two booleans, the `Settings`-container manifest, the
**v68.0+** version floor, and the retrieve-first gate check. The manifest template
the script generates is `assets/package.xml`.
> This uses the sf CLI Metadata API deploy, not a raw SOAP call. The member is the
> mdApiType **without** the `Settings` suffix (`ThunderbirdVoice`), and the manifest
> version MUST be **>= 68.0** — the fields are not exposed at any lower version.
4. **Change only the named target field(s), to the requested state.** The deployed
settings file sets exactly the preference(s) the user named — `enableCallRecording`
and/or `enableCallTranscription` — to the requested value (`true` to enable, `false`
to disable); every other `ThunderbirdVoiceSettings` boolean, INCLUDING a target
preference the user did not name, keeps its retrieved value. Do not add or flip
other booleans, and never coerce a disable request back to `true` — they are out of
this skill's scope or contrary to the request.
5. **Return the confirmation JSON.** Emit a single JSON object matching
`assets/confirmation-output.json`: the metadata `type` and `fullName`, that it was
applied via a headless/API deploy, and — for **each** of the two preferences
separately — its field name and its resulting enabled state (`true`/`false`, as it
now stands after the deploy, whether it was changed or left as-is), plus an overall
`status` (`ENABLED`, `DISABLED`, or `CONFIGURED`). It also
includes a **`preserved`** array echoing every sibling boolean (e.g.
`enableCallRecRedaction`, `enableSipRecording`) with the value it was retrieved
with — this attests the siblings were carried through **unchanged**, which
compliance callers need as explicit "nothing else moved" assurance. Reporting a
sibling's value is NOT changing it. On a successful run the script prints this
object on stdout; surface it verbatim as the deliverable — do not strip the
`preserved` block.
---
## Rules / Constraints
| Constraint | Rationale |
|-----------|-----------|
| Model recording and transcription as two separate preferences | They are distinct booleans (`enableCallRecording`, `enableCallTranscription`); collapsing them loses the ability to enable one without the other and fails the output contract. |
| Deploy through the generic `Settings` container with member `ThunderbirdVoice` | `ThunderbirdVoiceSettings` is not a retrievable top-level type name; the `Settings` container (member = mdApiType minus the `Settings` suffix) is the sf CLI path and uses the org's `sf` login — no session id. |
| Manifest version must be >= 68.0 | `ThunderbirdVoiceSettings` fields are only exposed at API v68.0+; below that the retrieve returns the type empty/absent. |
| Retrieve before deploy; preserve all sibling fields | The deploy is a full-file replace — starting from the retrieved file keeps other prefs intact and lets the gate be checked. |
| Report the preserved siblings in a `preserved` block, but never change them | Compliance callers need explicit "nothing else moved" assurance; the confirmation echoes each sibling's retrieved value unchanged. Echoing a value is not setting it — the deployed file changes only the named target field(s). |
| Honor the requested state literally — enable means `true`, disable means `false` | A disable request that silently re-enables the preference (or reports success while leaving it on) is a correctness failure; set exactly what was asked and report the value the org actually returns. |
| Verify by round-trip before reporting the status | A "Succeeded" deploy is not proof; only report the resulting state after a re-retrieve returns each named preference at its requested value. |
| Output must be a single valid JSON object | The confirmation is consumed programmatically by headless callers; prose-only answers are not usable. |
| Do not include a session id, access token, phone number, recording URL, or transcript content in the output | These are secrets / PII; the confirmation reports configuration state only. |
---
## Gotchas
| Issue | Resolution |
|-------|------------|
| `Property 'enableCallRecording' not valid in version <N>` / `Settings type 'ThunderbirdVoice' is unknown` | **API version too low.** The fields are only exposed at v68.0+. Raise `--api-version` to 68.0 or higher — this is NOT a feature gate. |
| Retrieve returns an empty `<ThunderbirdVoiceSettings/>` (no fields) | Org-perm gate `orgHasNativeVoiceAllowed` is off — Native Voice is not provisioned. Enable via provisioning/Blacktab; metadata cannot flip an unprovisioned feature. |
| Trying to retrieve `ThunderbirdVoiceSettings` as the type name | Use the `Settings` container with member `ThunderbirdVoice` (mdApiType minus the `Settings` suffix); the full type name is not a retrievable member. |
| Collapsing both prefs into one toggle (e.g. `recordingAndTranscription: true`) | Keep two distinct entries in both the deployed file and the confirmation JSON. |
| Deploying a partial settings file | Start from the retrieved file so sibling prefs keep their values; a partial file can reset them. |
| Missing `jq` / `sf` | The script depends on them; install or run in an environment that has them (declared in `metadata.cliTools`). |
---
## Output Expectations
Deliverables:
- A single JSON confirmation object on stdout, shaped per `assets/confirmation-output.json`
— the metadata target, the headless/API deploy path, each of the two preferences
reported separately with its enabled state, a `preserved` array echoing every
sibling boolean (unchanged) so callers can confirm nothing else moved, and an
overall `status`.
No files are written to the org's source tree — the script works in a temporary SFDX
project and reports the result.
---
## Reference File Index
| File | When to read |
|------|-------------|
| `scripts/enable-recording-transcription.sh` | Step 2 — run to retrieve → set named field(s) → validate → deploy → verify (supports `--recording on\|off`, `--transcription on\|off`, `--dry-run`, `--api-version`, `--help`; defaults to both on). |
| `references/thunderbird-voice-settings.md` | Step 3 — for the metadata type, the two booleans, the `Settings` manifest, the v68.0+ floor, and the gate check. |
| `assets/package.xml` | Step 3 — the Metadata API manifest template the script generates; read to understand or hand-build the retrieve/deploy. |
| `assets/confirmation-output.json` | Step 5 — the exact shape of the JSON confirmation to return. |