Pricing Academy Blog

Build your own CRM with Claude Code

Build your own CRM with Claude Code in eight steps: a Leads object, your contacts from a spreadsheet, and a first email campaign to them.

Summarize with:
What you'll build

A Leads object in the ElasticFunnels CRM with its own fields and a four-stage pipeline, your contacts imported into it from a spreadsheet, and a welcome email campaign to them that you test on yourself before anything goes out. Claude Code runs the ef CLI (command-line tool) for every step and asks before it sends.

New to Claude Code? Claude Code for marketing shows what an agent does with your funnels, from a brief to a split-test result. For the why behind the send itself (which campaigns to run first, what inbox providers require), read email marketing campaigns.

You need Claude Code, installed and signed in, Node.js 18 or newer, and the CLI at version 0.28.4 or later (ef --version tells you). In an empty folder, ef init signs you in through the browser and installs the skills Claude Code uses here: ef-crm, ef-campaigns and ef-segql. For the send you also need a connected sending account. Every flag and field is in the docs guide CRM, contacts and campaigns from Claude Code.

npm i -g @elasticfunnels/cli@latest
mkdir northwind && cd northwind
ef init
claude

1Ask Claude Code for a Leads CRM

Start claude in your bound folder and describe the CRM you want in one message: the object, its fields and the stages of its pipeline. Claude Code runs ef crm entities create for the object, then one ef crm fields create per field and the pipeline and stage commands.

The prompt asks for a Leads CRM with first name, last name, phone and source and a New, Contacted, Qualified, Customer pipeline; Claude Code creates the object, its fields, the pipeline and its stages with the ef CLI
The prompt: Make me a Leads CRM in ElasticFunnels: first name, last name, phone and source, and a pipeline: New, Contacted, Qualified, Customer. Reply in two sentences. The wait is sped up.

In the take the CLI printed ✓ Created CRM entity #120 (leads), then four fields (First name, Last name, Phone, Source) and ✓ Created pipeline #120 (sales) with its four stages. Claude Code summed it up as a Leads CRM (leads, #120) with a Sales pipeline that runs New → Contacted → Qualified → Customer. Keep the object's number; the campaign in step 6 uses it.

There is no email field yet, on purpose. The import in step 3 creates it from your file's email column, typed email. A pipeline puts the object in CRM mode, so every lead sits in a stage. A plain contact list without stages can be a Data object instead, which is what the docs guide uses.

2See the Leads object in the app

Open Apps › CRM. Leads is in the list of objects. Its Settings open on a Fields tab: every field Claude Code made, each with its type.

Apps, CRM lists the Leads object; its Settings open on the Fields tab with first name, last name, phone and source and their types
The CLI and the app read and write the same CRM, so you can keep shaping the object here: rename a field, add a stage.

3Import your contacts from a CSV file

Put the file in the folder and name it in the prompt. Claude Code runs ef crm import with --preview first: one line per column, the field it goes to, the field type it suggests and a few sample values. Nothing is written yet.

Claude Code previews the import of leads.csv as one line per column with its type, then, after the go, imports the rows with --yes and reports in one sentence that all 40 rows came in
First prompt: Import leads.csv into Leads (it has no entries yet). Show me the plan first: one line per column, "column: type", nothing else. Then: Looks right, import it. Then tell me in one sentence how many came in. The contacts are invented, with example.com addresses. Waits are sped up.

A CSV (comma-separated values) export from a spreadsheet or your old email tool works. So do TSV, TXT and XLSX files, with the column names in the first row. In the take, ef crm import leads --file leads.csv --preview read leads.csv: 40 rows, 8 columns. and Claude Code listed the plan as asked:

first_name: text
last_name: text
email: email
phone: phone
source: text
status: select
signup_date: date
tags: multiselect

Read it before you say yes:

  • The email column must show the type email, as it does here. The campaign finds the address through that type.
  • Each new field gets the suggested type. Correct a wrong one with --map "Column=key:type", or leave a column out with --skip.
  • Tags came out as multiselect because the samples look like lists. If your tag column isn't detected that way, force it with --map "Tags=tags:multiselect".
  • The entry title is the server's pick, usually a name or the email. --title sets the column yourself.

After your go, Claude Code runs ef crm import leads --file leads.csv --yes, because it has no terminal to answer the confirmation in. The CLI's output folds away on screen; its summary line reads ✓ Imported 40 entries into leads (0 skipped, 0 failed), and Claude Code's one sentence was: All 40 rows were imported into Leads, with none skipped or failed.

Importing the list again later? A plain re-import adds every row a second time. To update the people already there, run ef crm import <entity> --file list.csv --upsert-by email --yes: a filled cell overwrites the old value, an empty cell keeps it, and tags are combined. If a run stops partway it prints the row to resume from: run the same command with --offset and that number. One file holds up to 20 MB and 20,000 rows.

4Check the contacts in the app

Back in Apps › CRM, open Leads and its Entries tab. Each imported row is an entry, titled by a name or email column, with its values under the field columns.

The Entries tab of the Leads object lists the imported contacts with their example.com addresses and field values
The 40 entries from the import, each with its example.com address, in the pipeline's first stage. Open two or three and compare them with the file.

Imported entries don't fire stage automations or form webhooks, so nobody gets an email just because they were imported.

5Have Claude Code write the email

Ask for the email as a file: who it greets, what it says, where the button goes. Claude Code writes plain HTML into the folder, welcome.html here, which the campaign reads in the next step.

The prompt asks for a short welcome email greeting each lead by first name with a button to the quiz; Claude Code writes welcome.html
The prompt: Write a short welcome email for the Leads in welcome.html: greet them with {{ customer.first_name }}, two lines about Northwind, a button to northwind.example/quiz. Claude Code added a warning of its own: a lead with no first name saved gets "Hi ,".

Leave the footer out of the HTML. ElasticFunnels adds the unsubscribe link and your postal address (from Settings) to every campaign email when it goes out. The merge tag {{ customer.first_name }} reads the object's first_name field, so give your file a First name column. Without one it falls back to a name field and puts the whole name in the greeting.

6Create the campaign and count who gets it

Ask for a campaign to the Leads with that file and a subject, and ask how many people it reaches. Claude Code runs ef campaigns email create with --crm-entity and --html-file, then ef campaigns email audience-preview (short name count).

Claude Code creates a draft email campaign for the Leads collection with welcome.html and the subject Welcome to Northwind, then counts who will receive it and who is excluded
The prompt: Create an email campaign for the Leads CRM collection with welcome.html, subject "Welcome to Northwind". How many will get it? The wait is sped up.

The take printed ✓ Created draft email campaign #35. A campaign made from the CLI doesn't pick up the brand's default sender, so Claude Code named the sending account itself with --sender integration --integration and the account's id. If you have more than one, say in the prompt which one to use. The count is one line:

✓ 38 will receive it (of 40; excluded: 1 suppressed, 1 without consent, 0 duplicates)

The address comes from the object's field of type email, and a record without one is skipped. The three exclusions:

  • Suppressed: on your suppression list after a bounce, a complaint or an unsubscribe, or added there by hand.
  • Without consent: a recorded no to marketing email. In the demo list one lead had said no, and one address was on the suppression list, so 38 of the 40 get it.
  • Duplicates: the same address in two entries goes out once.

Someone with no consent record at all counts as opted in. That's why the list you import should hold only people who agreed to hear from you.

Emailing buyers instead? A saved audience picks people by what they did, such as everyone who bought twice in a year. Claude Code writes the SegQL query (the ef-segql skill), saves it with ef audiences create, and the campaign takes --audience with that id in place of --crm-entity.

7Send yourself a test

Ask for a test. Claude Code runs ef campaigns email test, which sends one copy, footer included, to your own address and never to the list.

The prompt asks for a test; Claude Code runs ef campaigns email test and reports where the test was sent
Prompt: Send me a test of the Welcome to Northwind campaign. Reply in two sentences. Claude Code ran ef campaigns email test 35 --to [email protected], which printed ✓ Test sent to [email protected] via mailgun.

Without --to, the test goes to the address of the account that connected the CLI. You can send ten tests per campaign per hour. Open the draft under Campaigns to see what Claude Code set up. Its Review and send step reads "Collection: Leads, 38 will receive it" and "Test sent 2 minutes ago", and the button says Send to 38 people.

The campaign draft at Review and send: the audience is Collection: Leads, the test is marked as sent, and the send button is waiting
The same draft in the app. You can finish it here instead of in the terminal.

8Launch or schedule, after Claude Code asks

Say when it should go. Claude Code shows you the count and anything that still blocks the send, then waits for your yes before it runs launch or schedule with --yes. In the clip the answer is no, so the campaign stays a draft.

Asked to send to all the Leads now, Claude Code shows the count and asks for confirmation; the answer is not yet, and the campaign stays a draft
Asked first: Send the Welcome to Northwind campaign to all the Leads now. Claude Code ran no launch. It listed the campaign, with no blockers left, and asked: Reply "yes, send it" and I'll launch it to those 38 leads. The answer: No, not yet. Keep it as a draft. Its reply began: Okay, I haven't sent anything. Campaign #35 "Welcome to Northwind" is still a draft.

Two guards sit under that question. Without --yes, the CLI stops at a confirmation it can't get from Claude Code. And the app refuses to send a campaign that never had a test, unless --skip-test is passed.

  • Later: ef campaigns email schedule <id> --at 2026-12-01T15:00:00Z. The time must be more than a minute ahead; unschedule turns it back into a draft.
  • Stop it: cancel stops a scheduled or sending campaign. Emails already sent stay sent.
  • After the send: ask how it went. Claude Code runs ef campaigns email stats for delivered, opened, clicked, bounced and unsubscribed.

Common mistakes when you build your own CRM

  • Importing the same file twice. Without --upsert-by email, every row lands again. Resume a stopped run with the --offset it printed.
  • An email column typed as text. The campaign reads the field of type email first. Check the preview, and set the type with --map "Email=email:email" before the real run.
  • Importing people who never opted in. No consent record counts as yes, so the exclusions can't catch them. Only you can.
  • Pasting contacts into the chat. Hand Claude Code the file path. The ef-crm skill keeps it to counts and the plan.
  • Old Excel files. XLS and Numbers files are refused. Save them as XLSX or CSV.
  • Launching a draft that isn't ready. create lists what still blocks the send, such as a missing subject or sending account. ef campaigns email get <id> shows the list again.

Keep going

Every flag, field type and limit, as a reference: the docs guide CRM, contacts and campaigns from Claude Code. The same campaign built on the Campaigns screen, with an A/B test on the subject: how to create an email campaign. Let Claude Code edit your pages too: add an MCP server to Claude Code. What an agent does with your funnels: Claude Code for marketing. AI email marketing beyond one send: email marketing campaigns. Everything the CLI does: the ef CLI. All tutorials: the Academy.

FAQ

Build your own CRM FAQ

Can you create your own CRM?

Yes. You decide what a record is (a lead, a deal, a partner), which fields it has and which stages it moves through. In ElasticFunnels that is a CRM object with custom fields and an optional pipeline, and Claude Code can build it from one sentence.

How do you build a CRM system?

Start from the list you already have. Its columns tell you the fields, and the way you follow up tells you the stages. Create the object, import the list, check a few records by hand, then decide what you send and when.

Is there a free CRM I can use?

The CRM, the campaigns and the ef CLI are part of every ElasticFunnels plan, with no separate CRM fee. Claude Code needs a Claude account that can run it, and emails go out through the sending account you connect, which may bill you for volume.

What are the four types of CRM?

The usual list is operational (records, pipelines and follow-ups), analytical (reports on the data), collaborative (a shared record across teams) and strategic (built around the long-term customer relationship). The Leads object in this tutorial is an operational CRM with email attached.

Is CRM difficult to learn?

The ideas are simple: records, fields, stages. The work is in the setup, and that is the part Claude Code takes over here. Afterwards the object is an ordinary screen under Apps, CRM that anyone on the team can open.

Which files can I import?

CSV, TSV, TXT and XLSX, with the column names in the first row. An Excel file is read from its first sheet. One file holds up to 20 MB and 20,000 rows. Save old XLS or Numbers files as XLSX or CSV first.

What happens if I import the same file twice?

A plain re-import adds every row a second time. To update the people already there, add --upsert-by email: a row whose email matches an entry updates it, a filled cell overwrites the old value, an empty cell keeps it, and tags are combined. If a run stops partway, it prints the row to resume from; run the same command with --offset and that number.

Which column does the campaign send to?

The object's field of type email, or a field with the key email when there is none. A record without a valid address is skipped. The first-name merge tag reads a first_name field, so include a First name column; without one it falls back to a name field and inserts the whole name.

Who is left out when the campaign sends?

Addresses on your suppression list (bounced, complained, unsubscribed or added by hand), people with a recorded no to marketing email, and repeats of the same address. Someone with no consent record counts as opted in, so import only people who agreed to hear from you.

Can Claude Code send the campaign without asking me?

Launch and schedule stop at a confirmation unless --yes is passed, and the ef-campaigns skill tells Claude Code to show you the count and wait for your go before it adds it. The send is also refused while no test was sent, unless --skip-test is passed.

Do imported contacts trigger my automations?

No. Imported entries fire no stage automations and no form webhooks. They appear in the object like entries added by hand, so a welcome sequence for an imported list is a campaign you send, as in this tutorial.

Can I email buyers instead of a CRM list?

Yes. Ask Claude Code for a saved audience, for example everyone who bought twice in the last year. It writes the SegQL query with the ef-segql skill, saves it with ef audiences create, and the campaign takes --audience with the new id instead of --crm-entity.

Can I do the same without Claude Code?

Yes. In the app, open the object in Apps, CRM and use Import file on its Entries, then build the email under Campaigns. The campaign screen, step by step, is in how to create an email campaign.

How many test emails can I send?

Ten per campaign per hour. Without --to, the test goes to the address of the account that connected the CLI. A test never counts as a send to the list.

Build your CRM where your funnels are

Every feature on every plan. Start a 14-day free trial.