Handling sensitive content
Bayview Taxi’s dispatcher agent quotes fares, books rides, and takes payment for them. Only one of those three needs a card number, and the agent doesn’t have to be the thing that hears it.
That is the shape of most sensitive-content problems on a voice call. A value has to reach something during the conversation, but it rarely has to reach the language model. The platform gives you ways to collect it, speak it, and remember it while the model stays out of the loop, and content redaction for the cases where it genuinely can’t.
Decide what the agent needs to know
Before reaching for redaction, ask what the agent actually has to do with the value. Usually it needs an outcome, not the value itself. The dispatcher doesn’t need the card number; it needs to know whether the payment went through. It doesn’t need the caller’s full account number; it needs to know whether the account is valid.
Only the last row is a redaction problem. The rest are design choices.
Perform actions outside the agent’s context
The first four rows all work the same way. A SWAIG function returns an action, the platform carries out that action on the call, and the sensitive value moves between the caller, a SWML method, and your server without passing through the model. What the agent gets back is whatever outcome you choose to report.
Take a payment
The pay method collects card details through dual-tone multi-frequency (DTMF)
keypad input, validates them, and hands them to your payment connector. The digits are never spoken,
never transcribed, and never enter the conversation. Your agent triggers it from a SWAIG function
and learns only what you choose to tell it afterward.
The ai_response variable is the whole control surface here. When the payment finishes, whatever you
set it to is handed back to the agent as its next piece of context, and nothing else from the payment
flow is. Write a status into it and the agent knows the status. Write ${pay_result} and it knows the
result code. There is no field you could set that would leak the card number unless you put it there
yourself.
The Server SDK’s pay() sets a sensible ai_response for you, describing the result and telling the
agent not to discuss the collection itself. Override it when you want the agent to say something
specific about what happens next.
Collect digits
The same pattern works for anything a caller can type: an account number, a PIN, a policy number, the
last four digits of a card. The prompt method plays a message and collects DTMF
digits into prompt_value, and request posts them to your server.
save_variables turns the JSON your server replies with into SWML variables, which is where ${status}
comes from. The digits reach your server and your server alone.
ai_response is expanded against every variable in scope, prompt_value included. Writing
"You entered ${prompt_value}" puts the digits straight into the model’s context and undoes the
whole exercise. Report the verdict, not the input.
Speak a value
Sometimes the caller needs to hear a value read back. There are two ways to do that, and they differ in what ends up in the call’s records.
A say action hands text to the agent’s own voice, and the agent speaks it verbatim. It lands in
the call’s conversation record, and redaction will not mask it there:
redaction rewrites what the caller said and what the model generated, not text your handler supplied.
Anything sensitive in a say action shows up in your records exactly as you wrote it.
A SWML action playing say: text uses a separate text-to-speech pass outside the AI session
entirely. Nothing about it reaches the model, and nothing is added to the conversation unless you set
ai_response. Reach for this one when the value must stay out of both the model’s context and the
conversation record.
Remember a value
An agent that has to hold a value across several turns is an agent that has the value in its context. Store it beside the conversation instead, and let your handlers read it back.
Two stores are available, and the difference between them matters here.
meta_data is a keyed store rather than one shared bag. Each function carries a
meta_data_token, and every function carrying the same token reads and writes the same store, while a
function with a different token sees nothing of it. Leave the token off and SignalWire derives one from
that function’s web_hook_url together with the credentials you set for it, so two functions share a
store by default only when their handler URL and its credentials both match. Nothing in meta_data is
interpolated into the prompt, so nothing you put there reaches the model. This is the right place for a
payment token, a verified account ID, or anything else your handlers need and the conversation does not.
global_data is call-scoped state that your handlers also receive, but it is additionally made
available to the prompt for interpolation. A value in global_data reaches the model if, and only if,
your prompt references it by name. That makes it a good fit for things the agent genuinely should know
about, such as the caller’s first name or the fare it just quoted, and a poor fit for a card number.
A later function reads the token out of raw_data["meta_data"] and takes no arguments of its own,
which means there is no argument for the agent to get wrong or a caller to talk it out of.
Tool calling covers that pattern in full, and
state management covers the lifecycle of both stores.
Redact conversation records
Some conversations leave you no choice about the model hearing the value. A caller reads their card number aloud before the agent can offer the keypad. A health intake line has to take a date of birth in conversation. An agent has to confirm a value it was told earlier.
For those, content redaction rewrites the conversation text — what the caller says and what the agent generates — in the completed turns that reach AI events, webhook payloads, and the post-conversation call log. The conversation itself is untouched. The caller hears the agent normally and the agent understands the caller perfectly. Only the recorded text changes.
Recorded text is the limit of it: redaction reaches the text of a turn and not the structured fields the platform records beside that turn. Coverage is narrower than every record of the call, and what gets masked sets out every surface, masked and unmasked.
Enable redaction
Turn redaction on with a single parameter, redact_prompt, in the ai
method’s params block:
The value of redact_prompt does two jobs: it switches redaction on, and it describes in plain
language what counts as sensitive. Matching text is replaced with ---- in the conversation turns the
platform records and delivers, and only there — what gets masked has the full
list of surfaces.
Redaction rewrites the conversation text the platform records and transmits, not what the model processes. The agent still receives the caller’s real words on every turn, which is what keeps the conversation working. If your requirement is to keep a value away from the model, use one of the patterns above instead. Redaction is the fallback for when you can’t.
What gets masked
Redaction covers both sides of the conversation. Your redact_prompt description guides the
agent to treat matching content as sensitive whenever it speaks: the first time it says it,
when it repeats it back, and when it confirms it. What the caller says is masked separately, once the
turn is complete and before that turn is stored or delivered. Text your handler supplies is neither
of those, so a say action’s text is recorded as you wrote it.
The structured-field row is the one to design around. Alongside each turn’s text, the platform records
what it worked out about that turn, and none of that is rewritten. A turn where the caller read out a
phone number, a card number, or a social security number can carry an entity field holding that
value in canonical form, and a tool result that was shortened before the model saw it carries the full
original in original_result. Both travel with the turn into your webhook payloads and the call log.
Turn entries on the call timeline are built from these fields rather than from the turn’s own text, so
masking the text leaves them unchanged.
Interim events fire while a turn is still in progress, before there is a finished turn to mask, so an
application that receives them sees the caller’s raw words. Where no unmasked text may leave the
platform, keep those events out of your own systems and turn
transparent_barge off.
Timeline entries that record a transformation carry both the text before and the text after, so a transcription-cleanup, normalization, or pronunciation entry can hold the original alongside the masked version.
The SWAIG row is deliberate. Your handler is the code that has to act on the value, so the arguments the agent extracted arrive intact. What is masked in a SWAIG payload is the conversation text carried alongside them. Treat your own handler as a place where sensitive data lands, and log accordingly.
Redaction is performed by AI, not by a fixed pattern-matcher, which is why it catches a card number read back one digit at a time. Within the conversation text it errs on the side of masking too much rather than too little. It is not a control for keeping a value out of every record: where that is the requirement, keep the value out of the conversation using one of the patterns above.
Keep it fast
Redaction runs inline on the turn, not on a thread of its own. Masking a turn is a separate model call that has to finish before the turn does, so its latency lands in the pause the caller hears. Two companion parameters keep that work quick.
utility_model selects the model used for supporting passes like redaction and
transcription cleanup. It defaults to the agent’s main model, which is usually larger and slower
than these passes need.
Set utility_model to a small, fast model so redaction doesn’t add noticeable latency to the
agent’s responses. The utility_model reference names the values it accepts.
auto_correct cleans up the transcription of the caller’s speech, converting
spoken numbers to digits, formatting addresses and phone numbers, and fixing obvious mishearings.
When used alongside redact_prompt, cleanup and redaction happen together in a single step
instead of two.
auto_correct only takes effect when enable_text_normalization,
which is on by default, is set to "off". The example below includes both settings.
A complete configuration:
Verify redaction
Place a test call
Call your agent and read out a fake card number, such as 4242 4242 4242 4242, then let the
conversation run on for a few more turns.
Check the call records
Open the call in your Dashboard and review the logs. In the call_log and raw_call_log, the
conversation turns where the number was said, by you or by the agent, should read ----. Timeline
entries recording a text transformation can still show the original, as
What gets masked describes.
Limitations
Redaction is best-effort. It is AI-driven and biased toward over-masking, but a value can slip through, so treat it as a strong safeguard for your logs and integrations rather than a guarantee.
It also has nothing to say about audio. If you record calls, the recording still holds the real spoken words, and so does anything downstream that transcribes it.
Where a value lives is a separate decision from what gets logged. Anything that must outlive
the call belongs on your server. Anything that only matters during the call can go in
meta_data, which is gone when the session ends.