Guides

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.

| View as Markdown
Hunter Hodnett
Hunter Hodnett CPTO at Chipp
| 1 min read
# custom-actions # reliability # webhooks # tutorials

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 forLeave multi-use for
Lead / form submissionsDatabase lookups
Bookings and appointmentsKnowledge retrieval
Registrations and signupsSending chat messages back to the user
One-time payments and chargesTranslating or formatting text
Inviting a user to a groupWeb searches
Sending a single notificationRe-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

Open the Custom Action Editor

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

Fill in the Action Details

Set the name, description, URL, HTTP method, and parameters as you normally would.

3

Check the Single-Use Box

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

Save

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.

markdown
## 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:

plaintext
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:

ScenarioBlocked?
Model emits a second tool call in a later turnYes
Model retries after seeing a successful responseYes
Model retries after your webhook returned 500Yes
Request bounces to a different server replica during a deployYes
User starts a brand-new chat session with the same emailNo — this is a new session
Same user on two different devices at the same timeNo — 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:

plaintext
Header name:  Idempotency-Key
Header value: {{var.CHAT_SESSION_ID}}-submit-lead

Every 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.

python
# 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:

  1. 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.

  2. 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.

  3. 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.

  1. Check that your system prompt explicitly tells the model to call the action in the same response as the closing message.
  2. Read the tool description on the action. If it says “after” or “before” the closing message, change it to “in the same response as.”
  3. 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.

  1. 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.
  2. If both came from the same session, this is unusual. Open a support ticket with the session ID and we’ll investigate.
  3. 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.

  1. 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.
  2. 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.

  1. 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.”
  2. Move the action-calling instruction to the end of your prompt. Models weight later instructions slightly more heavily.