Single-Use Custom Actions
Prevent a custom action from firing more than once in the same conversation. Use for lead submissions, bookings, registrations, charges, and any one-shot side effect.
Some custom actions should fire exactly once per conversation. A lead submission. A booking. A registration. A charge. You want the AI to call the action after it has collected everything 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 (rarely) call it twice in a row. Without safeguards, your downstream system gets duplicate rows, duplicate emails, and duplicate charges.
Chipp has a built-in safeguard for this: the Only allow this action to run once per conversation checkbox in the Custom Action editor. When it’s on, the platform blocks a second call to the same action in the same chat session before the HTTP request leaves Chipp.
This guide walks you through enabling the checkbox, writing prompt language that works with it, and adding a second layer of protection on your receiving endpoint for the cases the platform guard can’t cover.
When To Use This
Turn the checkbox on for any action with a side effect you don’t want to repeat. Leave it off for any action the AI might legitimately call multiple times in one chat.
| Use single-use for | Leave multi-use for |
|---|---|
| Lead / form submissions | Database lookups |
| Bookings and appointments | Knowledge retrieval |
| Registrations and signups | Sending chat messages back to the user |
| One-time payments and charges | Translating or formatting text |
| Inviting a user to a group | Web searches |
| Sending a single notification | Re-reading a document |
If you’re unsure, start with the checkbox off (the default). Turn it on when you see a duplicate you didn’t want, or when the side effect is irreversible.
Enabling Single-Use
1
Go to your app’s Build page and open the Actions tab. Click Add Action to create a new action, or click an existing action to edit it.
2
Set the name, description, URL, HTTP method, and parameters as you normally would.
3
Below the URL field, check Only allow this action to run once per conversation.
The AI will only be able to call this action once per chat session. After the first successful call, any subsequent attempt is blocked without running your webhook.
4
Click Save. The setting takes effect immediately for all new conversations.
Writing Your Prompt
The checkbox prevents duplicates. It does not force the AI to call the action in the first place. Your prompt still needs to tell the model clearly when to call it.
Two prompt rules make a noticeable difference.
1. Be Specific About the Turn
Vague timing words like “at the end” or “after confirming” leave the model room to produce a closing message without ever emitting the tool call. The turn ends, the conversation ends, the submission never happens.
Be explicit that the tool call and the closing message should live in the same assistant response.
## Submission
Call the submit-lead 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 submission will never happen.2. Don’t 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, and whichever it picks will sometimes look like a bug.
Keep both short and matching. A good tool description reads:
Submits the lead to our CRM. Call this in the same assistant response
that delivers the final confirmation to the user. Required fields:
firstName, lastName, email, consent.What the Platform Guard Catches
With the checkbox on, a second call to the same action in the same chat session is blocked before any HTTP request is made. This covers most of the cases where 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 different devices at the same time | No — two sessions |
The guard is scoped to a single chat session. If two different people (or the same person in two different browsers) start two conversations and both go through the flow, each gets one submission. That is usually the correct behavior, but it’s worth knowing up front.
Adding True Exactly-Once (Advanced)
The platform guard handles the common cases. For actions where even a single duplicate is costly (charges, bookings with limited inventory, legal records), add a second layer of protection on your receiving endpoint.
The standard pattern is an idempotency key. Chipp sends a header on every request with a value unique to the logical operation. Your handler caches that key for some window and, on a repeat, returns the prior response without re-running side effects.
Sending an Idempotency Key From Chipp
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}}-submit-leadEvery call to this action from the same chat session will carry 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 recognize 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 responseThree things to get right:
-
The cache write and the 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. One reliable way: use a unique constraint on the idempotency key in the same database row as the side effect, and catch the constraint violation as your dedupe signal.
-
Cache the full response, not just a status code. Clients sometimes parse the response body. Returning a new 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 sometimes takes minutes (an LLM mid-turn, a slow proxy, a webhook retry queue).
Best Practices
Start with the checkbox. For 90% of single-use actions, the checkbox alone eliminates the duplicate problem. Only add idempotency on your server when the cost of a single slipped duplicate is significant.
Test the skip case, not just the duplicate case. The more common failure isn’t a duplicate, it’s the model skipping the call entirely. When testing, watch for conversations that should have submitted but didn’t, not just conversations that submitted twice.
Don’t rely on email or user ID uniqueness as your dedupe mechanism. Both feel like natural keys, but they break the moment the same person 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 tend to drift. Every time you edit the system prompt, re-read the tool description on your action and make sure the timing rules still agree.
Troubleshooting
The AI 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.
- Read the tool description on the action. 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’re still seeing two records.
- Check that both duplicates came from the same chat session ID. If they came from different sessions (different browsers, different days, page refreshes), the guard doesn’t apply. That’s two legitimate submissions.
- If both came from the same session, this is unusual. Open a support ticket with the session ID and we’ll investigate.
- For cases where the checkbox scope isn’t enough (different sessions, same user), add the idempotency key pattern described in the Advanced section.
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 instruction 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 AI 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 user has confirmed their email address and agreed to the terms.”
- Move the action-calling instruction to the end of your prompt. Models weight later instructions slightly more heavily.
Related Guides
- Custom Actions Overview — building your first action
- Agent Links — using actions to chain agents together
- Heartbeat — proactive outreach where actions also fire