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

# Run Code Steps

> Your Python in a workflow step: what it can read, how it branches, Generate with AI, Verify and Test

A Run code step is your Python, run on every message that reaches it. Call your own API or classifier, read the chat, compute a value — and the steps after it branch on what it returns.

```python theme={null}
async def run():
    text = x_message["text"].lower()
    return {"intent": "pricing" if "price" in text else "other"}
```

## The shape

* One `async def run():` at the top level, **with no parameters** — everything it needs is already in scope.
* **Return a dict.** It is kept under the step's **result key** for the rest of the run: later steps read a field as `{{key.field}}` in messages, as a **Code result** in conditions and contact updates, and other code steps as `x_results["key"]["field"]`.
* **Success or failure.** Return `{"status": "error", "message": "..."}` to take the **Failure** exit. Raising, a refused code check and the timeout take it too. Anything else takes **Success**.
* **Timeout** up to 30 seconds per step (10 by default); a message's code steps share 60 seconds. Give every network call a shorter timeout of its own.

## What is in scope

| Name | Holds |
| - | - |
| `x_message` | The message: `text`, `type`, `button_id`, `media_link`, `msg_id` (below) |
| `x_history` | The chat, oldest first, ending with this message — as many messages as the Conversation page's context length |
| `x_results` | What earlier code steps returned in this run, by key. A step that did not run is missing: use `.get()` |
| `x_contact` | `exists`, `phone_number`, `channels`, and `attributes` — your contact attributes by name |
| `x_context` | `user_phone_number`, `user_phone_number_with_plus`, `agent_phone_number`, `user_id`, `agent_id`, `msg_id`, `current_time`, `current_date`, `timezone`, `is_test` |
| `x_secrets` | Your Secrets Vault, decrypted: `x_secrets["NAME"]` (**Settings → Secrets**) |
| `update_contact_custom` | Saves contact attributes: `await update_contact_custom({"lead_stage": "hot"})` |
| `httpx`, `json`, `asyncio` | Already imported. `re`, `math`, `datetime`, `hashlib`, `base64`, `urllib.parse` and similar can be imported |

### `x_message` by what the user sent

| The user sends | `type` | `text` | `media_link` |
| - | - | - | - |
| Text | `text` | the text | — |
| A voice note | `audio` | its transcript (with Audio Transcription on), else `[audio]` | — |
| An image | `image` | its caption, or `[image]` | the image (with Image Context on) |
| A document | `document` | its caption or file name, and its link | the file |
| A video, a sticker | `video`, `sticker` | `[video]`, `[sticker]` | — |
| A location | `location` | `Location: lat, lng \| name \| address` | — |
| A tapped button | `interactive` (`button` for a template's quick reply) | the button's title | — (`button_id` is set) |

### `x_history`

Each message has `role` (`user` or `assistant`), `text`, `type`, `time`, `msg_id`, `media_link`, and for replies who sent them: `agent_id` (the agent), `workflow_node` (the workflow step), `flow_step` (the Advanced agent step). Count or search the chat with it instead of keeping state:

```python theme={null}
answers = sum(1 for m in x_history if m["role"] == "assistant" and m["agent_id"] == "AGENT_ID")
```

### `update_contact_custom`

Writes to the contact of this chat. Keys are your contact attribute names; a name that is not an attribute is skipped and listed in `skipped`. `x_contact` and every later step see the new values at once. In Test Chat nothing is saved.

```python theme={null}
count = int(x_contact["attributes"].get("other_messages_count") or 0) + 1
result = await update_contact_custom({"other_messages_count": count})
# {"status": "success", "updated": ["other_messages_count"], "skipped": []}
```

## Writing it: Generate with AI, Verify, Test

| | |
| - | - |
| **i** | The full guide: the shape, every input, limits, the code checks, examples |
| **Generate with AI** / **Edit with AI** | Describe what the step should do. The AI knows what is in scope, your secret and attribute names, and the other code steps' keys. On a new step the code goes straight in; on existing code you review the change side by side first. Generated code is verified as soon as you take it |
| **Verify** | Runs the same checks as saving a chat function — the code policy and a review for anything that could reach our servers. The step shows **Verified** until the code changes |
| **Test** | Runs the code with a sample message you type, and shows what it returns and how long it took |

Every code step is also verified when you press **Update** (only the ones that changed and were not verified yet), and checked again every time it runs. Code that breaks the rules is never run: the step takes its Failure exit.

## What the code checks refuse

* Modules that reach the server: `os`, `sys`, `subprocess`, `socket`, `pathlib`, `shutil`, `importlib`, `pickle`, `io`, `tempfile`, `multiprocessing` and similar — directly or through another module.
* `open`, `eval`, `exec`, `compile`, `__import__`, `globals`, `vars`, `dir`; `getattr` with a computed name; dunder attributes.
* Environment variables, and the platform's own packages (`utils`, `apps`, `database`, `redis`, `pymongo`, `clickhouse_connect`, `azure`, `boto3`…).
* Hard-coded keys, tokens or passwords — save them in **Settings → Secrets** and read them with `x_secrets`.

## Testing safely

In Test Chat, code runs for real, the user's phone number is `0000000000`, and contact writes are not saved. Branch on `x_context["is_test"]` for anything that must not happen in a test — creating a real payment link, booking, messaging someone:

```python theme={null}
if x_context["is_test"]:
    return {"link": "https://example.com/test-payment-link"}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.