How to Build a Full CRM Automation with .0n SWITCH Files in 10 Minutes

# How to Build a Full CRM Automation with .0n SWITCH Files in 10 Minutes Here's the scenario: a new contact hits your CRM. Within the next 90 seconds, you need to enrich their data, score them against your ICP, route them to the right pipeline stage, send a personalized welcome email, and notify your sales rep on Slack. In a traditional CRM workflow builder, this is 20+ clicks, a dozen modal dialogs, a filter condition editor that makes you want to close the laptop, and 45 minutes of your afternoon. In 0nMCP, it's a `.0n` SWITCH file and one command. Let me show you exactly how. --- ## What Is a .0n SWITCH File? A SWITCH file is a structured JSON document that describes an automation as a series of steps. It follows the `.0n` Standard — a universal config format for AI-driven workflows. Think of it as the infrastructure-as-code equivalent for business automations. Every SWITCH file has: - **`$0n`** — Document metadata (version, type, name) - **`trigger`** — What kicks off the automation - **`inputs`** — User-facing configuration variables - **`launch_codes`** — Sensitive credentials (API keys, tokens) - **`steps`** — The ordered list of actions to execute Variable resolution follows a strict priority order: ``` {{system.*}} > {{launch.*}} > {{inputs.*}} > {{step.output.*}} ``` System variables are always trusted first. Launch codes (secrets) override input values. Step outputs are referenced by step ID. --- ## The Automation We're Building Here's what we want to happen when a new contact is created in the CRM: 1. **Enrich** — Pull company data, LinkedIn, tech stack from People Data Labs 2. **Score** — Check if they match our ICP (B2B, 10-500 employees, uses SaaS tools) 3. **Route** — Move them to "High Priority" or "Nurture" pipeline based on score 4. **Email** — Send a personalized welcome email using their first name and company 5. **Notify** — Post to the `#new-leads` Slack channel with a summary card --- ## The Complete SWITCH File ```json { "$0n": { "version": "1.1.0", "type": "workflow", "name": "new-contact-icp-router", "description": "Enrich, score, route, email, and notify on every new CRM contact", "created": "2026-02-27" }, "trigger": { "type": "webhook", "event": "contact.created", "source": "crm" }, "inputs": { "pipeline_id_hot": "your-hot-pipeline-id", "pipeline_id_nurture": "your-nurture-pipeline-id", "icp_min_employees": 10, "icp_max_employees": 500, "sender_name": "Mike from 0nMCP", "sender_email": "mike@rocketopp.com", "slack_channel": "#new-leads" }, "launch_codes": { "pdl_api_key": "{{env.PDL_API_KEY}}", "crm_api_key": "{{env.CRM_API_KEY}}", "slack_token": "{{env.SLACK_BOT_TOKEN}}" }, "steps": [ { "id": "enrich", "name": "Enrich Contact Data", "service": "pdl", "action": "enrich_person", "params": { "email": "{{trigger.contact.email}}", "first_name": "{{trigger.contact.first_name}}", "last_name": "{{trigger.contact.last_name}}", "company": "{{trigger.contact.company_name}}" }, "on_error": "continue" }, { "id": "score", "name": "Score Against ICP", "service": "internal", "action": "condition", "params": { "conditions": [ { "field": "{{step.enrich.output.employee_count}}", "operator": "between", "min": "{{inputs.icp_min_employees}}", "max": "{{inputs.icp_max_employees}}" }, { "field": "{{step.enrich.output.job_company_type}}", "operator": "equals", "value": "private" } ], "logic": "AND", "output_true": "hot", "output_false": "nurture" }, "depends_on": "enrich" }, { "id": "route", "name": "Move to Correct Pipeline", "service": "crm", "action": "create_opportunity", "params": { "contact_id": "{{trigger.contact.id}}", "pipeline_id": "{{step.score.output == 'hot' ? inputs.pipeline_id_hot : inputs.pipeline_id_nurture}}", "name": "{{trigger.contact.first_name}} {{trigger.contact.last_name}} — New Lead", "status": "open" }, "depends_on": "score" }, { "id": "welcome_email", "name": "Send Personalized Welcome Email", "service": "crm", "action": "send_email", "params": { "contact_id": "{{trigger.contact.id}}", "from_name": "{{inputs.sender_name}}", "from_email": "{{inputs.sender_email}}", "subject": "Hey {{trigger.contact.first_name}} — welcome to the ecosystem", "body_html": "
Hi {{trigger.contact.first_name}},

Saw you just joined — welcome. We work with a lot of teams at {{step.enrich.output.job_company_name || trigger.contact.company_name}} size doing exactly what you're describing.

I put you in the right place. You'll hear from us shortly.

— {{inputs.sender_name}}
" }, "depends_on": "route" }, { "id": "slack_notify", "name": "Notify Sales Team on Slack", "service": "slack", "action": "post_message", "params": { "channel": "{{inputs.slack_channel}}", "text": "New {{step.score.output}} lead: *{{trigger.contact.first_name}} {{trigger.contact.last_name}}* at *{{step.enrich.output.job_company_name || trigger.contact.company_name}}*\n• Employees: {{step.enrich.output.employee_count || 'unknown'}}\n• Title: {{trigger.contact.title || 'unknown'}}\n• Email: {{trigger.contact.email}}\n• Pipeline: {{step.score.output == 'hot' ? 'HIGH PRIORITY' : 'Nurture'}}", "unfurl_links": false }, "depends_on": "route" } ] } ``` --- ## Breaking Down What's Happening ### The Trigger Block ```json "trigger": { "type": "webhook", "event": "contact.created", "source": "crm" } ``` This wires the automation to the CRM's webhook system. When a contact is created, the CRM fires a POST to your 0nMCP HTTP server endpoint. The trigger payload is available as `{{trigger.*}}` throughout all steps. ### Variable Resolution in Action Notice how `{{trigger.contact.email}}` pulls from the webhook payload. `{{inputs.sender_name}}` pulls from your static config. `{{step.enrich.output.employee_count}}` pulls from the runtime output of the previous step. The `||` operator handles graceful fallbacks: if enrichment didn't find a company name, fall back to what the CRM already has. ### The `on_error: continue` Pattern Step 1 (enrich) uses `on_error: continue`. PDL enrichment can fail if the email doesn't have a match. Instead of aborting the entire workflow, we continue — the downstream steps handle missing data gracefully via fallback expressions. ### `depends_on` for Step Ordering Steps 3 (`route`), 4 (`welcome_email`), and 5 (`slack_notify`) all depend on prior steps. `depends_on` guarantees execution order. Steps 4 and 5 both depend on step 3 but not each other — in Assembly Line mode, they run in parallel after routing completes. --- ## Running the Automation ```bash # Execute directly against a test payload 0nmcp run new-contact-icp-router.0n --input contact.email=test@example.com # Or start the HTTP server and let the CRM webhook trigger it 0nmcp serve --port 3001 ``` For webhook-triggered automations in production: ```bash 0nmcp serve --port 3001 --host 0.0.0.0 ``` Then point your CRM webhook to: `https://your-server.com/webhook/contact.created` --- ## The 20-Click Alternative For reference, here's what building this same automation in a traditional CRM workflow builder looks like: 1. Open Automation Builder 2. Create new workflow 3. Name it 4. Choose trigger type (contact created) 5. Add filter conditions 6. Add HTTP action (for PDL enrichment) 7. Configure URL, headers, body (separate modal) 8. Map response fields (JSON path picker) 9. Add If/Else branch 10. Configure branch conditions 11. Add "Update Contact" action for hot branch 12. Add "Update Contact" action for nurture branch 13. Configure pipeline ID dropdown 14. Add email action 15. Open email template editor 16. Write HTML email 17. Map personalization tokens 18. Set sender details 19. Add Slack action 20. Authenticate Slack app 21. Configure channel + message format 22. Publish and test That's 22 steps, spread across 4-6 different modal dialogs, with no version control, no code review, no reusability, and no portability. Your `.0n` SWITCH file is 80 lines of JSON. It's in your git repo. It's readable by humans and machines. It runs identically in dev, staging, and production. --- ## Adding It to Your Master SWITCH If you've run `0nmcp engine import` to set up your master credentials, you can reference your services directly: ```bash # Verify your credentials are loaded 0nmcp engine verify # Run the workflow against your real services 0nmcp run new-contact-icp-router.0n ``` The engine maps your imported credentials to `{{launch.*}}` variables automatically. No copy-pasting API keys into config files. --- ## What Comes Next This is one automation. But the pattern scales. Every business process you're clicking through manually is a SWITCH file waiting to be written. Once it's a file, it's: - **Versionable** — git history for every change - **Portable** — runs anywhere 0nMCP is installed - **Composable** — combine SWITCH files into larger pipelines - **AI-editable** — hand it to Claude and say "add a step that scores by company size" — it knows the schema That last point is the one that changes everything. When your automation infrastructure is data (JSON files), AI can read it, modify it, extend it, and reason about it. When it's a drag-and-drop UI state, AI can't touch it. Write the file. Run the file. Ship the outcome. **[Get 0nMCP](https://www.npmjs.com/package/0nmcp) | [.0n Standard docs](/0n-standard) | [More examples](/examples)**