Back to The Times of Claw

Build Your Own CRM with OpenClaw from Scratch

Build a custom CRM with OpenClaw from scratch: define objects, create fields, write skills, and query your DuckDB data — complete step-by-step guide.

Mark Rachapoom
Mark Rachapoom
·8 min read
Build Your Own CRM with OpenClaw from Scratch

Building a custom CRM with OpenClaw takes about an hour. You define your data model, write a skill that teaches the agent how to work with it, and you have a CRM that fits your workflow exactly — not a vendor's template. Here's the complete walkthrough.

Before starting, make sure DenchClaw is installed (npx denchclaw) and running. The full setup guide covers the initial installation.

Why Build Your Own?#

Every off-the-shelf CRM is built for someone else's sales process. Salesforce assumes you have stages, opportunities, and territories. HubSpot assumes you're doing inbound marketing. These assumptions show up as required fields, locked views, and workflow logic you can't disable.

OpenClaw's approach is different: start with an empty DuckDB database and define exactly what your business tracks. A consulting firm tracks different things than a SaaS startup. A recruiting agency tracks different things than a real estate office. With OpenClaw, you model your actual process — not a vendor's generic approximation of it.

The result is a CRM that your agent understands completely, because you wrote the schema and the skill that describes it.

Step 1: Plan Your Data Model#

Before touching code, sketch your model on paper. A CRM is fundamentally about:

  1. Who — the people and organizations you track
  2. What — the deals, projects, or opportunities you manage
  3. When — the activities and events that happen
  4. Why — the notes, context, and history that explains decisions

For a straightforward B2B SaaS CRM, this might look like:

Contacts (people)
  - name, email, phone, title
  - company (→ Companies)
  - status: Lead | Prospect | Customer | Churned
  - lead_source: inbound | outbound | referral | event
  - lead_score: 0-100

Companies (organizations)
  - name, domain, size, industry
  - arr: annual recurring revenue
  - tier: SMB | Mid-Market | Enterprise

Deals (opportunities)
  - title, value, close_date
  - contact (→ Contacts)
  - company (→ Companies)
  - stage: Discovery | Demo | Proposal | Negotiation | Closed Won | Closed Lost
  - probability: 0-100

Activities (events)
  - type: call | email | meeting | note
  - date, duration, summary
  - related_contact (→ Contacts)
  - related_deal (→ Deals)

Write this down. You'll reference it throughout.

Step 2: Understand the DenchClaw Schema#

DenchClaw uses an EAV (Entity-Attribute-Value) schema in DuckDB. There are four core tables:

  • objects — your entity types (Contact, Company, Deal, Activity)
  • fields — the field definitions for each type
  • entries — individual records
  • entry_fields — the values of each field for each record

There are also PIVOT views (v_contacts, v_companies, etc.) that flatten EAV into readable rows.

You don't have to work with raw EAV — the views do that work. But understanding the underlying model helps when writing custom SQL.

Step 3: Create Your Object Types#

Connect to your DuckDB database and create the object types:

duckdb ~/.openclaw-dench/workspace/workspace.duckdb
-- Create object types
INSERT INTO objects (id, name, slug, created_at) VALUES
  (gen_random_uuid(), 'Contact', 'contact', NOW()),
  (gen_random_uuid(), 'Company', 'company', NOW()),
  (gen_random_uuid(), 'Deal', 'deal', NOW()),
  (gen_random_uuid(), 'Activity', 'activity', NOW());

Step 4: Define Your Fields#

For each object type, define the fields:

-- Contact fields
INSERT INTO fields (id, object_id, field_key, field_label, field_type, field_options) 
SELECT 
  gen_random_uuid(),
  id,
  unnest(['email', 'phone', 'title', 'company_id', 'status', 'lead_source', 'lead_score']),
  unnest(['Email', 'Phone', 'Title', 'Company', 'Status', 'Lead Source', 'Lead Score']),
  unnest(['text', 'text', 'text', 'reference', 'enum', 'enum', 'integer']),
  unnest([NULL, NULL, NULL, NULL, 
    '["Lead","Prospect","Customer","Churned"]',
    '["inbound","outbound","referral","event"]',
    NULL])
FROM objects WHERE slug = 'contact';
 
-- Deal fields
INSERT INTO fields (id, object_id, field_key, field_label, field_type, field_options)
SELECT
  gen_random_uuid(),
  id,
  unnest(['value', 'close_date', 'stage', 'probability', 'contact_id', 'company_id']),
  unnest(['Value ($)', 'Close Date', 'Stage', 'Probability (%)', 'Contact', 'Company']),
  unnest(['decimal', 'date', 'enum', 'integer', 'reference', 'reference']),
  unnest([NULL, NULL,
    '["Discovery","Demo","Proposal","Negotiation","Closed Won","Closed Lost"]',
    NULL, NULL, NULL])
FROM objects WHERE slug = 'deal';

Step 5: Create PIVOT Views#

The EAV tables are fast to write to but verbose to query. Create flattened views:

-- v_contacts view
CREATE OR REPLACE VIEW v_contacts AS
SELECT
  e.id,
  e.label AS name,
  MAX(CASE WHEN ef.field_key = 'email' THEN ef.field_value END) AS email,
  MAX(CASE WHEN ef.field_key = 'phone' THEN ef.field_value END) AS phone,
  MAX(CASE WHEN ef.field_key = 'title' THEN ef.field_value END) AS title,
  MAX(CASE WHEN ef.field_key = 'status' THEN ef.field_value END) AS status,
  MAX(CASE WHEN ef.field_key = 'lead_score' THEN ef.field_value::INTEGER END) AS lead_score,
  MAX(CASE WHEN ef.field_key = 'lead_source' THEN ef.field_value END) AS lead_source,
  e.created_at
FROM entries e
JOIN objects o ON e.object_id = o.id
LEFT JOIN entry_fields ef ON ef.entry_id = e.id
WHERE o.slug = 'contact'
GROUP BY e.id, e.label, e.created_at;
 
-- v_deals view
CREATE OR REPLACE VIEW v_deals AS
SELECT
  e.id,
  e.label AS title,
  MAX(CASE WHEN ef.field_key = 'value' THEN ef.field_value::DECIMAL END) AS value,
  MAX(CASE WHEN ef.field_key = 'close_date' THEN ef.field_value::DATE END) AS close_date,
  MAX(CASE WHEN ef.field_key = 'stage' THEN ef.field_value END) AS stage,
  MAX(CASE WHEN ef.field_key = 'probability' THEN ef.field_value::INTEGER END) AS probability,
  e.created_at
FROM entries e
JOIN objects o ON e.object_id = o.id
LEFT JOIN entry_fields ef ON ef.entry_id = e.id
WHERE o.slug = 'deal'
GROUP BY e.id, e.label, e.created_at;

Step 6: Write the CRM Skill#

Now the important part: writing the skill that teaches the agent how to work with your custom CRM.

Create ~/.openclaw-dench/workspace/skills/my-crm/SKILL.md:

# My CRM Skill
 
This skill manages a B2B SaaS CRM stored in DuckDB.
 
## Data Model
 
- **Contacts**: people you track (v_contacts view)
- **Companies**: organizations (v_companies view)
- **Deals**: sales opportunities (v_deals view)
- **Activities**: calls, emails, meetings (v_activities view)
 
## Common Queries
 
### Show Pipeline
```sql
SELECT title, stage, value, probability, close_date
FROM v_deals
WHERE stage NOT IN ('Closed Won', 'Closed Lost')
ORDER BY close_date ASC;

Weighted Pipeline Value#

SELECT 
  SUM(value * probability / 100) AS weighted_value,
  SUM(value) AS total_value,
  COUNT(*) AS open_deals
FROM v_deals
WHERE stage NOT IN ('Closed Won', 'Closed Lost');

Hot Leads (high score, no deal yet)#

SELECT c.name, c.email, c.lead_score, c.lead_source
FROM v_contacts c
LEFT JOIN v_deals d ON d.contact_id = c.id
WHERE c.status = 'Lead'
  AND c.lead_score > 70
  AND d.id IS NULL
ORDER BY c.lead_score DESC;

Creating Records#

When asked to add a contact:

  1. Ask for: name (required), email (required), company (optional), status (default: Lead)
  2. INSERT into entries with object_id for 'contact'
  3. INSERT each field value into entry_fields
  4. Confirm: "Added [name] as a new [status]."

When asked to create a deal:

  1. Ask for: title (required), value (required), contact (required), stage (default: Discovery)
  2. INSERT deal record and field values
  3. Confirm: "Created deal '[title]' for $[value] in [stage] stage."

Updating Records#

Always show current value before updating: "Current status: Lead. Update to Prospect?" Wait for confirmation before executing UPDATE.

Guard Clauses#

  • Never delete without explicit confirmation
  • Never bulk update more than 10 records at once without listing them
  • Always verify record exists before updating

Output Format#

Pipeline summaries: markdown table (Title, Stage, Value, Probability, Close Date) Contact lists: bullet list with name, email, company, status Deal counts/sums: plain text ("$2.4M weighted pipeline across 18 open deals")


---

## Step 7: Seed Initial Data

Import existing contacts from CSV:

```bash
duckdb ~/.openclaw-dench/workspace/workspace.duckdb << 'EOF'
-- Load contacts from CSV
CREATE TEMP TABLE import_contacts AS
SELECT * FROM read_csv_auto('/path/to/contacts.csv');

-- Insert into entries
INSERT INTO entries (id, object_id, label, created_at)
SELECT 
  gen_random_uuid(),
  (SELECT id FROM objects WHERE slug = 'contact'),
  name,
  NOW()
FROM import_contacts;

-- Insert email field values
INSERT INTO entry_fields (id, entry_id, field_key, field_value)
SELECT
  gen_random_uuid(),
  e.id,
  'email',
  ic.email
FROM entries e
JOIN import_contacts ic ON e.label = ic.name
JOIN objects o ON e.object_id = o.id
WHERE o.slug = 'contact';
EOF

Step 8: Test Your CRM#

Open DenchClaw and try these commands with your agent:

  • "Show me my pipeline"
  • "Add a new contact: Sarah Chen, sarah@acme.com, status: Lead"
  • "Create a deal: Acme Enterprise, $50k, contact Sarah Chen"
  • "What's my weighted pipeline value?"
  • "Show hot leads with no deals"

If any command fails or produces unexpected output, review your skill and refine the instructions.

Going Further#

Once your basic CRM is working:

  • Add the browser skill to enrich contacts with LinkedIn data
  • Add the himalaya skill to send follow-up emails from the agent
  • Build a dashboard app to visualize your pipeline
  • Write a custom reporting skill for weekly pipeline reviews

FAQ#

Can I import data from Salesforce or HubSpot? Yes. Export as CSV from either platform. The import script above handles the basics. For complex field mappings, write a brief custom script that transforms the export into DenchClaw's schema before importing.

Does DenchClaw support many-to-many relationships? The base EAV schema supports reference fields (foreign keys to other entries). For true many-to-many (a contact belonging to multiple companies), you'd add a junction table or store arrays in a JSON field.

How do I back up my CRM data? DuckDB's database file is at ~/.openclaw-dench/workspace/workspace.duckdb. Back it up like any file — copy it to cloud storage, add it to your backup tool, or export to CSV via DuckDB's COPY command.

Can the agent create reports automatically? Yes. Write a reporting skill that defines the queries and output formats for your standard reports. The agent can run reports on a schedule via the OpenClaw cron system.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA