# Build a daily call list

**The job:** every morning, a ranked list of who to contact today, in the tool
your reps already open.

**Who this is for:** any team doing outbound off inbound signal.

**What it costs you:** one scheduled script. No webhook, no state.

---

## One call gets you the list

```bash
curl -s "https://app.signal.geysera.com/signal-api/v1/visitors?min_intent=60&page_size=100" \
  -H "Authorization: Bearer $SIGNAL_API_KEY"
```

```json
{
  "visitors": [
    {
      "company_domain": "acme.com",
      "company_name": "Acme Corp",
      "classification": "lead",
      "intent_score": 82,
      "resolved_email": "dana@acme.com",
      "resolved_name": "Dana Cole",
      "resolved_title": "VP Engineering",
      "employer": null,
      "visit_count": 7,
      "first_visit_at": "2026-08-02T09:14:00Z",
      "last_visit_at": "2026-09-15T16:41:00Z",
      "is_locked": false
    }
  ],
  "total": 214,
  "page": 1,
  "page_size": 100
}
```

`total` is what your plan may resolve, not the raw row count. If you are near
your cap it will be smaller than reality, and the list is still the best
visitors — not an arbitrary slice.

## Ranking it like a human would

`intent_score` alone puts a curious researcher above a returning buyer. What
reps actually want is *warm and moving*:

```python
from datetime import datetime, timezone

def rank(v):
    days_since = (datetime.now(timezone.utc)
                  - datetime.fromisoformat(v["last_visit_at"])).days
    recency = max(0, 30 - days_since) / 30          # 1.0 today, 0 a month ago
    depth = min(v["visit_count"], 10) / 10           # saturates; 40 visits is a bot
    return v["intent_score"] * (0.5 + 0.3 * recency + 0.2 * depth)

todays = sorted(visitors, key=rank, reverse=True)[:25]
```

Every term here is a judgement you should change. The point is that the
ranking is *yours* and lives in twelve lines you can read.

## Handing it to an LLM

```
Here are today's 25 highest-intent visitors:
{visitors_json}

Group them into: (1) call today, (2) email today, (3) leave alone. For each in
groups 1 and 2, give one sentence on why, citing a specific number from the
record. Put anyone in group 3 whose data does not justify contact, and say
what is missing.
```

Asking for group 3 explicitly is what stops a model finding a reason for all
twenty-five.

## What will go wrong

**`is_locked: true`.** Beyond your plan's monthly resolution cap, records come
back locked — you can see that a visitor exists but not who. Filter them out of
a call list rather than showing a rep a row they cannot action.

**Null names and titles.** `resolved_name`, `resolved_title` and `employer` are
frequently null. Identification gives you an email reliably; the rest is
enrichment and is not guaranteed. Write your templates to survive nulls.

**The same person every day.** Nothing here tracks who you already called. Keep
your own contacted set, or use the assistant's `set_visitor_contacted` so the
state lives with the visitor.

---

Next: [write a first-touch email from what they read](./write-a-first-touch-email-from-what-they-read.md) ·
[export and keep in sync](./export-and-keep-in-sync.md)
