# Single-Use Custom Actions

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.

## When to Use This

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.

## Enabling Single-Use

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

## Writing Your Prompt

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

```markdown
## 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.
```

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

## Adding True Exactly-Once (Advanced)

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.

```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. **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.

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

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

## Troubleshooting

### The Agent 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. Re-read the tool description. 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 still see two records.

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

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 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 agent 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 tech has confirmed the room ID and provided a severity level."
2. Move the action-calling instruction to the end of your prompt. Models weight later instructions slightly more.

## Related Guides

- Custom Actions Overview. Building your first action.
- [Agent Links](/docs/guides/agent-links). Using actions to chain agents together.
- [Heartbeat](/docs/guides/heartbeat). Proactive outreach where actions also fire.

*AVCodex · Your AV expertise. Amplified by AI.*
