Read and propose CRM changes
ctx.crm is one API over the tenant’s CRM, whichever it is. It speaks in Motions’ own field names, such as first_name and tax_id, and crm.yaml maps them to your CRM’s properties. A step written against it works on the test CRM and on HubSpot alike.
Grant it in motion.yaml
Section titled “Grant it in motion.yaml”A step only reads the objects it lists, and only proposes the actions it lists:
- id: contacts run: steps/contacts.ts crm: { read: [companies, contacts], propose: [contacts.create, associate] }| Call | Returns |
|---|---|
companies.get(id) |
a company, or null |
companies.search({ tax_id?, tax_root?, domain?, name?, limit? }) |
companies that match every field given |
contacts.list({ company }) |
a company’s contacts |
contacts.search({ email?, linkedin?, name? }) |
contacts that match |
deals.list({ company, open?, limit? }) |
a company’s deals, only open ones with open: true |
owners.get(id), owners.list() |
CRM users who own records, with name and email |
notes.list({ company? , contact? }) |
notes on a company or a contact |
Searches compare the way you’d expect: tax_id ignores punctuation, tax_root matches the start of the digits (a CNPJ’s 8-digit root finds every branch), domain ignores https:// and www., and name matches part of the name, ignoring case. limit defaults to 20, at most 100.
Records come back in Motions’ shape:
type CrmCompany = { id: string; name: string; domain: string | null; tax_id: string | null; owner_id: string | null; fields: Record<string, any> };type CrmContact = { id: string; first_name: string | null; last_name: string | null; email: string | null; title: string | null; linkedin: string | null; owner_id: string | null; company_ids: string[]; fields: Record<string, any> };type CrmDeal = { id: string; name: string; stage: string | null; amount: number | null; is_closed: boolean; owner_id: string | null; company_ids: string[]; fields: Record<string, any> };fields holds the custom properties crm.yaml maps for that object, by their short names.
The company the step is running for is already in company.core and company.fields; read it again only if you need fields crm.yaml doesn’t map.
Propose
Section titled “Propose”Writes return a reference to the proposed operation and change nothing yet. The changeset step after this one gathers them for an approver.
| Call | Proposes |
|---|---|
companies.update(id, { fields }) |
new values for custom fields marked writable: true in crm.yaml |
contacts.create(values) |
a contact; it needs an email or a linkedin URL |
contacts.update(id, values) |
new values for a contact |
associate(contact, company) |
linking a contact to a company |
notes.create({ on, body }) |
a note on a company or a contact |
notes.pin(note) |
pinning a note |
Contact values are first_name, last_name, email, title, linkedin, and fields for custom properties marked writable: true.
Use a reference wherever an id goes, to build on a record that doesn’t exist yet:
const contact = await ctx.crm.contacts.create({ first_name: p.first_name, last_name: p.last_name, email: p.email, title: p.title });await ctx.crm.associate(contact, company.core.crm.id);await ctx.crm.notes.create({ on: contact, body: `Found by Motions: ${p.why}` });When a proposal is made, Motions checks it against the CRM, refusing an update to a record that doesn’t exist or a field that isn’t writable, and records the current values so the approver sees before and after. contacts.create for an e-mail or LinkedIn URL that already exists is written as skipped.
Make fields writable
Section titled “Make fields writable”Custom fields are read-only unless crm.yaml says otherwise:
companies: fields: icp_tier: { from: icp_tier, type: enum, writable: true }The test CRM
Section titled “The test CRM”The built-in test CRM, system: memory in crm.yaml, is a CRM Motions keeps itself. It’s where you try motions before HubSpot, and the only one that writes today. Load records into it with crm-seed.json, keyed by object, with each record’s properties under your CRM’s property names, and the lists batches open on:
{ "owners": [{ "id": "o1", "properties": { "name": "Ana Souza", "email": "ana@orbita.example" } }], "companies": [{ "id": "201", "properties": { "name": "Metalúrgica Aurora", "domain": "aurorametais.example", "hubspot_owner_id": "o1" } }], "contacts": [{ "id": "c1", "properties": { "firstname": "Rita", "email": "rita@aurorametais.example" }, "company_ids": ["201"] }], "lists": { "first-batch": ["201"] }}The seed loads on the first push into an empty test CRM. npx motions push --reseed replaces everything in it.
In tests
Section titled “In tests”runStep takes a crm option with records in Motions’ shape, and returns what the step proposed as proposals. See Test a motion.
Motions is built by Pruma. These docs describe what the platform does today; Limitations lists what it doesn't do yet. Open the app