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

# Examples

> A question limit with Jev, button replies, media and spam — workflows in front of a sales agent

## Limit off-topic questions with Jev

A sales agent should answer questions, but not forever: after 5 questions outside the sales flow, the lead gets the payment link instead of another answer — with no LLM call.

```
Start → Run code: ask Jev ──success──▶ Condition: over 5? ──yes──▶ Run code: payment link ──▶ Send text {{pay.message}} → End
                         └─failure──▶ Assign sales agent      └─otherwise──▶ Assign sales agent
```

**1. Create the contact attribute and the secret**

**Contacts → Attributes**: add `other_messages_count`, a number. **Settings → Secrets**: add your OpenRouter key as `OPENROUTER_API_KEY`.

**2. Ask Jev**

A Run code step with the key `jev`. [Jev](https://openrouter.ai/blog/insights/what-is-jev/) is TypeSafe's typed model on OpenRouter: it takes a `state` and typed questions, and returns a probability per question — cheap, and fast enough to run on every message.

```python theme={null}
async def run():
    key = x_secrets.get("OPENROUTER_API_KEY", "")
    count = int(float(x_contact["attributes"].get("other_messages_count") or 0))
    if not key:
        return {"status": "error", "message": "Add OPENROUTER_API_KEY in Settings > Secrets"}
    state = {
        "latest_message": x_message["text"],
        "recent_chat": [{"from": m["role"], "text": m["text"]} for m in x_history[-9:-1]],
    }
    questions = {
        "outside_flow": {"type": "noul", "instructions": (
            "Does `latest_message` ask a question that needs an answer, rather than just moving "
            "the sales chat forward (a greeting, yes, no, ok, a tapped button, saying it is paid)?")},
    }
    async with httpx.AsyncClient(timeout=8) as client:
        resp = await client.post(
            "https://openrouter.ai/api/alpha/decisions",
            json={"model": "typesafe/jev-1.13", "state": state, "questions": questions},
            headers={"Authorization": f"Bearer {key}"},
        )
    if resp.status_code >= 400:
        return {"status": "error", "message": f"Jev HTTP {resp.status_code}"}
    p = float(resp.json()["answers"]["outside_flow"]["noul"])
    if p < 0.5:
        return {"action": "agent", "count": count}
    count += 1
    await update_contact_custom({"other_messages_count": count})
    return {"action": "link" if count > 5 else "agent", "count": count}
```

A `choice` question can return the chat's language in the same call, so the payment message is in Hindi, Tamil or English.

**3. Route on the answer**

A Condition: *A code result* · `jev` · `action` · equals · `link` → the payment link. **Otherwise** → **Assign chat agent** (your sales agent). Connect the Jev step's **Failure** exit to the agent too, so a Jev outage changes nothing for the lead.

**4. Send the link**

A Run code step (key `pay`) that creates the link with your payments API and returns `{"message": "Pay here: …"}` — a stand-in link when `x_context["is_test"]` — then **Send text** `{{pay.message}}` and **End**.

> [!TIP]
>
> If the agent has a question limit of its own in its prompt, remove it: the workflow is now the one counting.

## Branch on button taps

Buttons are not branches: a tap is the user's next message, with the button's id. Put a Condition right after Start:

| Rule | Goes to |
| - | - |
| The message · Tapped button id · equals · `buy` | Update contact (`lead_stage` = `checkout`) → the payment link |
| The message · Tapped button id · equals · `talk` | Assign chat agent |
| Otherwise | The rest of the workflow |

## Photos and voice notes

| Rule | Goes to |
| - | - |
| The message · Its type · matches the pattern · `^(image\|video\|sticker)$` | Assign chat agent — no classifier call for a photo, and it is never counted as a question |
| The message · Its type · equals · `audio` | Through the workflow as text — with **Audio Transcription** on, `x_message["text"]` is the transcript |

Keep **Image Context** on when the agent reads images (payment screenshots, for example): the workflow's own setting decides whether the image is kept for the agent.

## Stay quiet on spam

A Condition: *A count over the chat* · Times the user sent this exact text · last 2 hours · is greater than · `2` → **End**. The third identical message gets no reply, and costs nothing.

## A complete workflow to import

The [Build with Claude](/chat-agents/workflow/build-with-claude) skill ends with a complete workflow that uses every step type — buttons, a media rule, a spam counter, a classifier with a question limit, a template, a price list, a payment link and an assigned agent — ready to import and adapt.


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