There are three access patterns to Cloudflare, each for a different job.
Path 1: CRM API (operator)
The Worker API handles all writes and enforces business logic. The CRMClient wrapper handles auth:
pythonfrom crm_client import CRMClient
c = CRMClient()
c.post("/api/contacts", {"company": "...", "city": "Plano", ...})
c.post(f"/api/contacts/{contact_id}/notes", {"body": "Left voicemail."})
c.safe_update(contact_id, {"status": "contacted"})
safe_update does read-merge-write to avoid the destructive PUT where omitted fields get nulled.
Path 2: D1 direct SQL (analyst)
The CRM API answers pre-built questions. Direct SQL answers any question:
sql-- Pipeline by city and lead type
SELECT city, lead_type, COUNT(*) as count
FROM contacts
WHERE type = 'prospect'
GROUP BY city, lead_type
ORDER BY city, count DESC;
-- Contacts with no notes in 30 days
SELECT c.id, c.company, c.city, c.status
FROM contacts c
LEFT JOIN notes n ON n.contact_id = c.id
AND n.created_at > date('now', '-30 days')
WHERE c.type = 'prospect'
AND c.status != 'closed'
AND n.id IS NULL;
The D1 token is read-only. Aggregation runs server-side on Cloudflare, so the agent receives query results instead of raw contact rows.
Path 3: R2 aerials (scout)
This path is for satellite images of prospect parking lots. Before a call, the agent fetches the aerial and reads the lot: size, layout, space count, and visible condition.
lot_aerial.py geocodes an address, fetches a 3x3 grid of satellite tiles at zoom 20, and stitches them into a JPEG:
pythondef fetch_aerial(address, output_path, zoom=20):
lat, lon = geocode(address)
cx, cy = _latlon_to_tile(lat, lon, zoom)
tiles = []
for dy in range(-1, 2):
row = []
for dx in range(-1, 2):
url = f"https://mt1.google.com/vt/lyrs=s&x={cx+dx}&y={cy+dy}&z={zoom}"
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15)
row.append(Image.open(BytesIO(resp.content)))
tiles.append(row)
tw, th = tiles[0][0].size
out = Image.new("RGB", (tw * 3, th * 3))
for dy in range(3):
for dx in range(3):
out.paste(tiles[dy][dx], (dx * tw, dy * th))
out.save(output_path, "JPEG", quality=90)
After fetch, the image uploads via POST /api/contacts/{id}/images and the contact gets has_aerial = 1. Batch mode fills in all contacts missing aerials.
Summary
| Path | Direction | Use case |
|---|---|---|
| CRM API | Read + write | Push prospects, log notes, update statuses |
| D1 SQL | Read-only | Pipeline analysis, coverage gaps, prioritization |
| R2 aerials | Read + write via Worker | Lot scouting before calls |
