Twenty CRM: The Self-Hosted CRM We Turned Into a Control Plane for AI Agents

Most teams bolt an AI agent onto a CRM as a side chatbot. We did the opposite: the CRM became the control plane and the agents became the workers. What Twenty is (open-source, self-hosted, AGPL), how to connect agents via REST/GraphQL and webhooks, and our own cases — data enrichment, outreach, and internal automation driven by a task queue.

Twenty CRM: The Self-Hosted CRM We Turned Into a Control Plane for AI Agents
Contents

When people say "connect AI to your CRM," they usually mean a chatbot answering questions on top of the database. We took a different route. For us the CRM is not a contact directory — it is a control plane: it holds the canonical state of every process and a queue of tasks, and the agents are workers that clear that queue. Below: what Twenty is, why self-hosted specifically, how to connect agents to it, and three of our own cases — data enrichment, outreach, and internal automation.

What Twenty CRM is

Twenty is an open-source CRM you can run on your own server. It positions itself as the open alternative to Salesforce: the same "companies — people — deals — tasks" model, but the code is open and the data lives in your database, not someone else's cloud. Deployment is via Docker Compose: server, background worker, PostgreSQL and Redis come up as one stack.

Technically it is a TypeScript application: a NestJS backend with BullMQ queues, PostgreSQL for storage, Redis for queues and cache, a React frontend. For us that is a familiar stack with no exotics — the same foundation we build the rest of our products on.

The feature we picked it for is the open data model. You add your own objects and fields not as a workaround but as first-class entities, and they immediately become available over the API on equal footing with the built-in ones.

What self-hosted means — and why it is an advantage

Self-hosted means the CRM runs on your server, not the vendor's, and all the data is physically yours. For a business that has three concrete consequences: customer data never leaves your perimeter, there is no monthly per-seat fee, and nobody can raise your price or gate the API behind a tier.

Twenty's license is GNU AGPL v3. That is the same "free forever" license that guarantees the code stays open even when someone runs it as a cloud service. The self-hosted version cannot suddenly be made paid retroactively.

But this needs a precision reviews usually skip: Twenty is not 100% AGPL. Some files are marked @license Enterprise and carved out from AGPL under a separate commercial license — these are Enterprise Edition features that require a paid subscription in production. The core is open and self-sufficient; but if you specifically need enterprise features, that part is no longer "free forever." When planning a rollout, look at exactly what you need, not just at the words "open source."

For our scenario — the CRM as a control plane for agents — the open core is enough. We need custom fields, custom objects and the API, and those are the AGPL part.

What Twenty's API looks like — and why it is the whole point for agents

Twenty auto-generates a REST and a GraphQL API from your workspace's data model — there is no separate "static" API, because each workspace has its own schema. Add a custom object (say, "Invoice") and it instantly gets REST and GraphQL endpoints identical to the built-in "Company" or "Person."

The API splits in two. The Core API is CRUD over records: people, companies, opportunities, tasks and your custom objects, with filtering and relation traversal. The Metadata API manages the schema itself: creating and modifying objects, fields and relations programmatically. So an agent can not only read and write data but also shape the structure to fit the process.

Authentication is by an API key you create under Settings → API & Webhooks. The agent puts it in a header and works against /rest/ or /graphql/ like any other client.

And the last detail that makes the CRM a control plane rather than a passive database: webhooks. Twenty can send real-time notifications when a record is created or changed. That means the agent does not have to poll every second — it can be woken by an event: a new lead, a changed stage, a closed deal.

Our approach: the CRM is the control plane, agents are the workers

The idea is simple: state lives in the CRM, work lives in tasks, and an agent is a worker that takes a task, does it, and writes the outcome back. The CRM becomes the single source of truth, and the agents become interchangeable hands. Here are the three entities the whole thing rests on.

  • A custom state field on the company — the canonical status of the process (for example, a lead's stage in outreach). This is what deduplication and the "what next" decision run on.
  • The native Task object — the unit of work: channel, worker key, status (queued / in progress / done), due time, attempt count.
  • A worker key on the task — a small field that tells the controller which agent skill to hand this task to.

The most important principle: "task done" is not "success." The task status only says it was processed; whether it worked is recorded in the company's state field. This avoids the classic trap of confusing "the bot ran" with "the result was achieved."

A practical bonus of this shape is extensibility without rewrites. Adding a new channel (say, email alongside a messenger) means writing a new worker skill and a new key; the controller that hands out tasks does not change at all. It just looks at the key and routes the task to the matching worker.

Case 1: data-enrichment tasks

Enrichment is when an agent takes records with an empty field, finds the value, and writes it back, leaving already-filled ones alone. A classic chore that takes months by hand and goes stale anyway. In our workspace this is how companies' contact channels get filled in.

The mechanics are simple and idempotent: the agent queries, via the Core API, the companies where the needed field is empty, processes them in batches, and writes each find straight back to the CRM. If the process is interrupted, the next run simply picks up the ones still empty. State lives in the CRM, so re-running breaks nothing and duplicates nothing.

A concrete figure from our own workspace (a base of ~480 companies): in a single enrichment pass we lifted coverage of one contact channel from roughly 34% to 50% of records. This is not a bought database — it is filling in what was already there, by an agent instead of a person.

Case 2: outreach through a task queue

Our outreach is not a script that "messages everyone" — it is a controller on top of a task queue in Twenty. It runs in two modes: one builds the queue (creates tasks from a lead worklist, idempotently — already-processed ones are skipped), the other clears it (takes tasks in the "queued" status and hands each to a worker by its channel key).

The controller itself talks to no one — it only reads and writes the CRM and dispatches. All the channel-specific logic lives in a separate worker skill. So adding a channel is a new skill, not a rework of the controller.

Deduplication runs on the lead's state field, not on the fact of sending: if the stage is no longer "queued," the task just closes with no action. There is a daily cap on cold messages, an attempt counter, and a "verify first, then record" rule — state updates only after a confirmed action. And when a lead replies with something the agent is unsure about, the task does not invent an answer — it escalates to a human.

The point here is not automation for its own sake — it is that all the state is transparent and lives in the CRM. At any moment you can see who was messaged, who replied, what was escalated, and what stage each lead is at. It is not a bot's black box — it is a queue you can open and read.

How this generalizes to any internal automation

The same frame fits almost any repetitive internal process, not just outreach or enrichment. The recipe is one: a custom field for state, a native task for the unit of work, an agent as the worker, and a webhook or a schedule as the trigger.

Client onboarding (stages as a field, steps as tasks), inbound lead qualification, preparing data before a call or meeting, reminders and status checks, syncing with other systems — all map onto this cleanly. Anywhere there is "take something in state A, do an action, move it to state B," the CRM becomes the control plane and the agent becomes the hand.

The gain is not "a robot works instead of a person." The gain is that the process stops being invisible. State is no longer scattered across heads, chats and spreadsheets — it is in one place, machine-readable, with history. The human does not disappear in this scheme: they handle exactly what the agent flagged as "unsure" and escalated.

Limits of the approach: when Twenty as a control plane is overkill

This scheme is not free to maintain, and there are cases where it is overkill. Self-hosted means the server, backups and updates are on you; if there is no one to run that, a managed SaaS CRM will be cheaper in total effort. If your processes are few and rare, building a task queue is pointless — a simple script, or even manual work, will cost less.

Twenty's enterprise features, as noted above, require a paid subscription — so before a rollout it is worth checking whether anything you need falls into that part. And the "CRM as a control plane" approach pays off when processes are many and repetitive; on one or two it adds complexity without matching return.

We build this where the volume of routine is real and growing. At small volumes the more sensible advice is to start simple and add complexity only when the manual work genuinely piles up.