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 automationinputs— User-facing configuration variableslaunch_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:
- Enrich — Pull company data, LinkedIn, tech stack from People Data Labs
- Score — Check if they match our ICP (B2B, 10-500 employees, uses SaaS tools)
- Route — Move them to "High Priority" or "Nurture" pipeline based on score
- Email — Send a personalized welcome email using their first name and company
- Notify — Post to the
#new-leadsSlack channel with a summary card
The Complete SWITCH File
{
"$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": "<p>Hi {{trigger.contact.first_name}},</p><p>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.</p><p>I put you in the right place. You'll hear from us shortly.</p><p>— {{inputs.sender_name}}</p>" }, "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
"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
# 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:
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:
- Open Automation Builder
- Create new workflow
- Name it
- Choose trigger type (contact created)
- Add filter conditions
- Add HTTP action (for PDL enrichment)
- Configure URL, headers, body (separate modal)
- Map response fields (JSON path picker)
- Add If/Else branch
- Configure branch conditions
- Add "Update Contact" action for hot branch
- Add "Update Contact" action for nurture branch
- Configure pipeline ID dropdown
- Add email action
- Open email template editor
- Write HTML email
- Map personalization tokens
- Set sender details
- Add Slack action
- Authenticate Slack app
- Configure channel + message format
- 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:
# 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.