Stop a Custom Action from firing more than once in the same conversation. Use it for ticket submissions, RFP intake, dispatch requests, single-charge billing, anything with a one-shot side effect you do not want repeated.
Some Custom Actions should fire exactly once per conversation. An RFP submission. A dispatch ticket. A single-charge billing event. A site-survey booking. You want the agent to call the action after it has collected what it needs, and you never want a duplicate.
LLMs are non-deterministic. Given the same prompt twice, a well-behaved model might call your action correctly both times, skip the call the second time, or call it twice in a row. Without safeguards, your downstream system gets duplicate tickets, duplicate emails, duplicate truck rolls.
AVCodex has a built-in safeguard: the Only allow this action to run once per conversation checkbox in the Custom Action editor. When it is on, the platform blocks a second call to the same action in the same chat session before the HTTP request leaves AVCodex.
This guide covers how to enable the checkbox, write prompt language that works with it, and add a second layer of protection on your endpoint for the cases the platform guard cannot cover.
Turn the checkbox on for any action with a side effect you do not want repeated. Leave it off for any action the agent might legitimately call multiple times in one chat.
| Use single-use for | Leave multi-use for |
|---|---|
| RFP and lead submissions | Database lookups |
| Dispatch tickets and truck-roll requests | Knowledge retrieval |
| Site-survey bookings | Sending chat messages back to the user |
| One-time charges | Translating or formatting text |
| Inviting a tech to a project Circle | Web searches |
| Sending a single Slack notification to dispatch | Re-reading a manufacturer spec sheet |
If you are unsure, start with the checkbox off (the default). Turn it on when you see a duplicate you did not want, or when the side effect is irreversible.
1. Open the Custom Action Editor
Open your agent's Build page and the Actions tab. Click Add Action to create one, or click an existing action to edit it.
2. Fill in the Action Details
Name, description, URL, HTTP method, parameters. Standard.
3. Check the Single-Use Box
Below the URL field, check Only allow this action to run once per conversation.
The agent can call the action only once per chat session. After the first successful call, every subsequent attempt is blocked without running your webhook.
4. Save
Click Save. The setting takes effect immediately for new conversations.
The checkbox prevents duplicates. It does not force the agent to call the action in the first place. Your prompt still needs to say clearly when to call it.
Two prompt rules make a real difference.
1. Be Specific About the Turn#
Vague timing words like "at the end" or "after confirming" leave the model room to write a closing message without ever emitting the tool call. The turn ends, the conversation ends, the dispatch ticket never goes out.
Be explicit that the tool call and the closing message live in the same assistant response.
## Submission
Call the open_dispatch_ticket tool in the SAME assistant response that
delivers the closing confirmation message. Emit the tool call and the
message together, in one turn.
Do NOT deliver the closing message first and then call the tool in a
later turn. If you finish the closing message without having called
the tool in that same turn, the dispatch ticket will never be created.
2. Do Not Contradict the Tool's Own Description#
The tool's description (set when you create the action) and your system prompt must agree on timing. If the description says "call before the confirmation" and the prompt says "submit after the confirmation," the model has to pick one. Whichever it picks will sometimes look like a bug.
Keep both short and matching. A good description reads:
Submits the dispatch ticket to ServiceNow. Call this in the same
assistant response that delivers the final confirmation to the tech.
Required fields: roomId, severity, summary, callerEmail.
With the checkbox on, a second call to the same action in the same chat session is blocked before any HTTP request goes out. This covers most ways an LLM might duplicate a call, including:
| Scenario | Blocked? |
|---|---|
| Model emits a second tool call in a later turn | Yes |
| Model retries after seeing a successful response | Yes |
| Model retries after your webhook returned 500 | Yes |
| Request bounces to a different server replica during a deploy | Yes |
| User starts a brand-new chat session with the same email | No (this is a new session) |
| Same user on two devices at once | No (two sessions) |
The guard is scoped to a single chat session. If two different techs (or the same tech in two browsers) start two conversations and both go through the flow, each gets one submission. That is usually the correct behavior, but worth knowing.
The platform guard handles the common cases. For actions where even one duplicate is costly (a billed truck roll, a limited inventory booking, a legally tracked record), add a second layer on your receiving endpoint.
The standard pattern is an idempotency key. AVCodex sends a header on every request with a value unique to the logical operation. Your handler caches that key for a window and, on a repeat, returns the prior response without re-running side effects.
Sending an Idempotency Key From AVCodex#
In your Custom Action, add a header named Idempotency-Key with a value built from the chat session. In the Variables tab, reference the built-in system variable for chat session:
Header name: Idempotency-Key
Header value: {{var.CHAT_SESSION_ID}}-open-dispatch-ticket
Every call to this action from the same chat session carries the same key. The first request writes a real record. Every retry or duplicate carries the same key as the first.
Handling the Key on Your Server#
Your webhook needs to spot the repeat and skip the side effect.
# Pseudocode
def handle_submission(request):
key = request.headers.get("Idempotency-Key")
if not key:
return process(request)
cached = cache.get(key)
if cached is not None:
# This key has been seen before. Return the prior response
# without re-running side effects.
return cached.response
response = process(request)
cache.set(key, response, ttl_seconds=3600) # 1 hour window
return response
Three things to get right:
- Cache write and side effect must be atomic. If you write to the database first and set the cache after, a crash in between leaves your system in a state where the side effect ran but a retry would run it again. A reliable approach: put a unique constraint on the idempotency key in the same row as the side effect, and treat the constraint violation as your dedupe signal.
- Cache the full response, not just a status code. Clients sometimes parse the body. Returning an empty body on a repeat can confuse them. Cache the actual body and headers, and replay them verbatim.
- Pick a cache window longer than any plausible retry. One hour is forgiving. Five minutes is too tight if anything in your call chain occasionally takes minutes (an LLM mid-turn, a slow proxy, a webhook retry queue).
Start with the checkbox. For 90% of single-use actions, the checkbox alone solves the duplicate problem. Add server-side idempotency only when the cost of a slipped duplicate is significant.
Test the skip case, not just the duplicate case. The more common failure is not a duplicate. It is the model skipping the call entirely. When testing, watch for conversations that should have submitted but did not, not just conversations that submitted twice.
Do not rely on email or user ID uniqueness as your dedupe mechanism. Both feel like natural keys, but they break the moment the same tech legitimately submits twice for two different reasons. The chat session ID is the right scope for single-use-per-conversation semantics.
Review your tool description after any prompt edit. Prompts and tool descriptions are edited separately and drift. Every time you edit the system prompt, re-read the tool description and confirm timing rules still agree.
The Agent Never Calls the Action#
The action is configured correctly, the checkbox is on, but the model never fires the tool at all.
- Check that your system prompt explicitly tells the model to call the action in the same response as the closing message.
- Re-read the tool description. If it says "after" or "before" the closing message, change it to "in the same response as."
- Look at a transcript where the call was skipped. If the final assistant message is your full closing template and there is no tool call, the model treated the template as the end of the turn. Tighten the prompt language as shown above.
Duplicate Submissions Still Show Up#
The checkbox is on, but you still see two records.
- Confirm both duplicates came from the same chat session ID. If they came from different sessions (different browsers, different days, page refreshes), the guard does not apply. That is two legitimate submissions.
- If both came from the same session, this is unusual. Open a support ticket with the session ID and we will investigate.
- For cases where the checkbox scope is not enough (different sessions, same user), add the idempotency key pattern above.
The Model Keeps Retrying After Errors#
Your webhook returned 400 or 500, and the model re-emits the tool call trying to recover.
- In your tool description, add:
If this action returns an error, do NOT retry. Tell the user there was a submission issue and end the conversation.The model follows this reliably. - With the checkbox on, the retry would be blocked anyway, but the explicit instruction saves the user from a confusing experience.
The Action Fires Mid-Conversation#
The agent calls the action halfway through a conversation instead of at the end.
- The model is misinterpreting your timing language. Replace phrases like "when enough information has been collected" with a specific, observable condition: "after the tech has confirmed the room ID and provided a severity level."
- Move the action-calling instruction to the end of your prompt. Models weight later instructions slightly more.
- Custom Actions Overview. Building your first action.
- Agent Links. Using actions to chain agents together.
- Heartbeat. Proactive outreach where actions also fire.
*AVCodex · Your AV expertise. Amplified by AI.*