> ## Documentation Index
> Fetch the complete documentation index at: https://docs.callkaro.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Functions

A custom function is your own Python, running inside the conversation. It is the escape hatch: anything the built-in function types do not cover — an API call, a database lookup, a calculation, a write into another system.

Custom functions are available on **both** WhatsApp and Instagram agents.

## Function shape

```python theme={null}
@tool
async def get_order_status(order_id: str) -> dict:
    """
    Look up the current status of a customer's order.
    Use when the customer asks where their order is or when it will arrive.
    """
    import httpx

    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.get(
            f"https://api.example.com/orders/{order_id}",
            headers={"Authorization": f"Bearer {x_secrets['api_key']}"},
        )

    if response.status_code != 200:
        return {"error": "Could not reach the order system"}

    return response.json()
```

Four rules the runtime enforces:

<AccordionGroup>
  <Accordion title="Start with @tool, then async def" icon="at">
    Every function begins with the `@tool` decorator followed by an `async def`. The function name here must match the **Name** field on the form.
  </Accordion>

  <Accordion title="The docstring is the tool description" icon="quote-left">
    This is what the model reads when deciding whether to call the function. Write it for the model: what it does, and when to use it. **A vague docstring is the most common reason a function never gets called.**
  </Accordion>

  <Accordion title="Imports go inside the function" icon="box">
    Nothing may sit above `@tool`. Put every `import` in the function body.
  </Accordion>

  <Accordion title="Return a dict" icon="brackets-curly">
    Always. Return a readable error dict rather than raising — a raised exception reaches the customer as a generic failure with nothing useful in it.
  </Accordion>
</AccordionGroup>

## Parameters

Type-annotate every parameter. The model fills them from the conversation, and the **Test Run** tab reads the same signature to build its input fields.

Name parameters after the value they hold — `city`, `order_id` — not `data` or `value`. The name is part of what the model uses to work out what to put there.

## Direct Send

<Note>
  WhatsApp agents only.
</Note>

Off by default. With Direct Send **on**, the function's return value is sent to the customer as-is, with no second model call — so it must be exactly:

```python theme={null}
{"status": ..., "message": ...}
```

`message` is the text the customer receives, verbatim.

With Direct Send **off**, the model reads your dict and writes its own reply, so any shape works.

<Tip>
  Use Direct Send when the wording must be exact and unedited — a legal disclaimer, a reference number, an OTP. Leave it off when you want the agent to weave the result into a natural reply.
</Tip>

## Execution timeout

`5` to `120` seconds, default **30**, set on the Basic tab. The process is terminated if it exceeds the limit.

Set your own HTTP client timeout **below** it, as in the example above, so a slow API returns a clean error you control instead of being killed mid-call.

## Secrets

`x_secrets` is a dict of every secret in your account's Secrets Vault, already decrypted:

```python theme={null}
api_key = x_secrets["api_key"]

# .get() returns None instead of raising if the name is missing
api_key = x_secrets.get("api_key")
```

<Warning>
  Never hardcode API keys or credentials in a function. Save them in the [Secrets Vault](/agents/configurations/secrets) and read them from `x_secrets`.

  Attribute access (`x_secrets.api_key`) raises `AttributeError` — it is a dict.
</Warning>

## Conversation context

`x_context` is available in every function with no lookup. The phone number keys use the same names as the post-call function variables on voice agents.

```python theme={null}
x_context["user_phone_number"]              # "919876543210"
x_context["user_phone_number_with_plus"]    # "+919876543210"
x_context["agent_phone_number"]             # your WhatsApp number
x_context["agent_phone_number_with_plus"]
x_context["agent_phone_number_id"]
x_context["user_id"], x_context["agent_id"], x_context["msg_id"]
x_context["channel"]                        # "whatsapp" | "instagram"
x_context["current_time"]                   # "2026-08-29T19:47:50+05:30"
x_context["current_date"]                   # "2026-08-29"
x_context["timezone"]                       # "Asia/Kolkata"
x_context["contact"]                        # same object as x_contact
```

## The contact record

`x_contact` is this person's CRM contact row, already loaded — querying it yourself only costs latency.

```python theme={null}
x_contact["exists"]        # False if this person has no contact record yet
x_contact["first_name"], ["last_name"], ["full_name"], ["email"]
x_contact["lead_stage"], ["lead_direction"], ["lead_source"]
x_contact["conversion_status"], ["tags"], ["channels"]

# Your own contact attributes, flattened to plain values
city = x_contact["attributes"].get("city")

# .get() everywhere — a brand-new contact has empty strings, not missing keys
name = x_contact.get("full_name") or "there"
```

If the agent calls `update_contact` earlier in the same turn, `x_contact` reflects that change — a function running after it always sees the updated values, never stale ones.

## Building and testing

| Tab          | What it is for                                                                                            |
| ------------ | --------------------------------------------------------------------------------------------------------- |
| **Code**     | The editor, plus **Generate with AI** / **Edit with AI** to draft or change a function from a description |
| **Basic**    | Name, description, execution timeout, Direct Send                                                         |
| **Test Run** | Input fields built from your function signature, and the actual result of running it                      |

<Tip>
  Test Run executes the real function against the real systems it calls. Point it at a test record the first time, not a live customer's order.
</Tip>

## Related

<CardGroup cols={2}>
  <Card title="Secrets" icon="key" href="/agents/configurations/secrets">
    Where credentials belong
  </Card>

  <Card title="Functions overview" icon="code" href="/chat-agents/functions/overview">
    The other function types
  </Card>
</CardGroup>
