Build a headless CRM your agent runs
Companies, contacts, deals, and every call or email, linked in one Base76 base. Your agent does the logging; you get pipeline totals and contact pages.
Small-team CRMs usually die because nobody logs anything. Here the agent does the logging. Paste in an email thread or your call notes and it files the contact, the company, the deal, and the touchpoint, linked together. You never open a form.
The prompt
Set up a CRM base in Base76. Companies have a unique name, a domain, and notes. Contacts have a name, a unique email, a role, and a company. Deals have a title, a company, a stage (lead, qualified, proposal, won, lost), a value in USD, and a close date. Interactions have a summary, a contact, a kind (call, email, meeting), a date, and notes. When a contact names a company we don't have yet, create it.
The schema
Four collections, linked like this:
| From | Field | To | Inverse | On delete |
|---|---|---|---|---|
contacts | company | companies | contacts | set_null |
deals | company | companies | deals | restrict |
interactions | contact | contacts | interactions | cascade |
companies.name and contacts.email are marked unique, which lets them stand in for record ids. The last sentence of the prompt becomes on_missing: "create" on the contact’s company field:
{ "key": "company", "type": "relation",
"options": { "to": "companies", "inverse": "contacts",
"on_delete": "set_null", "on_missing": "create" } }
The delete rules matter. Deleting a contact takes their interactions with them (cascade). Deleting a company that still has deals is refused (restrict), so a deal can’t lose its company by accident.
Logging without looking things up
Because of the unique fields, the agent links records by name or email and skips the lookup round trip:
curl -s https://api.base76.com/v1/bases/tim/crm/records/interactions \
-H "Authorization: Bearer $B76_KEY" \
-H 'content-type: application/json' \
-d '{"record": {"summary": "Sent proposal for the ordering portal",
"contact": {"email": "maya@northwindbakery.example"},
"kind": "email", "date": "2026-10-14"}}'
A new contact with "company": {"name": "Northwind Bakery"} creates the company if it doesn’t exist yet. Deals have no on_missing, so a deal naming an unknown company fails with an error instead of inventing one.
Pipeline totals
“What’s in the pipeline?” becomes an aggregate call. Totals are computed in the base, so the agent doesn’t pull every deal into the chat to add them up:
{"collection": "deals",
"where": {"stage": {"nin": ["won", "lost"]}},
"group_by": ["stage"],
"metrics": {"deals": {"count": true}, "pipeline": {"sum": "value"}}}
{"groups": [
{"stage": "lead", "deals": 1, "pipeline": 2500},
{"stage": "proposal", "deals": 1, "pipeline": 4800},
{"stage": "qualified", "deals": 1, "pipeline": 6200}],
"truncated": false}
A find with include walks the links in one call. This returns companies with a deal at the proposal stage, along with their people and all their deals:
{"collection": "companies",
"where": {"deals": {"some": {"stage": "proposal"}}},
"select": ["name"],
"include": {"contacts": {"select": ["name", "email"]},
"deals": {"select": ["title", "stage", "value"],
"sort": "-value"}}}
A contact page with the whole history
The contact template puts the person on top and their interactions underneath as a timeline, newest first:
[{"type": "title"},
{"type": "meta", "fields": ["role", "company"]},
{"type": "fields", "fields": ["email"]},
{"type": "related", "field": "interactions", "style": "timeline",
"sort": "-date", "heading": "History"}]
A company page can list its people and deals the same way, with two related blocks in table style. Your teammates see these pages in the app.
For a teammate who should work the pipeline with you, the agent calls add_guest with their email and a role: editor to change records, viewer to read. Guests need a Base76 account, and the Free plan allows five guests per base. After that, every write result reports the wider audience, so the agent knows it’s no longer writing to a private base.
Limits
- No email or calendar sync. The agent logs what you give it.
- Nothing runs on a schedule. Follow-up reminders come from your agent or a script that queries
dealsbyclose_date. - A guest sees the whole base. To show someone one deal or one contact, send a share link for that record instead.