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.

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.

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.

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
multiselectbecause 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.
--titlesets 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.

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.

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).

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.

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.

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.

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;unscheduleturns it back into a draft. - Stop it:
cancelstops a scheduled or sending campaign. Emails already sent stay sent. - After the send: ask how it went. Claude Code runs
ef campaigns email statsfor 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--offsetit 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-crmskill 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.
createlists 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.