Skip to content

Tenant files

  • tenant.yaml
  • crm.yaml
  • crm-seed.json
  • Directoryfixtures/
    • Directorysources/
      • rdap.json
  • Directorymotions/
    • Directorydomain-check/
      • …
  • Directorysources/
    • Directorybrasilapi/
      • …
  • package.json
  • tsconfig.json
  • CLAUDE.md
  • .gitignore

motions check, push and test find the tenant by looking for tenant.yaml in the current folder and the ones above it, so they work from anywhere inside it. --tenant <folder> points them somewhere else.

tenant.yaml
id: orbita
title: Órbita Software
timezone: America/Sao_Paulo
# Built-in sources answer from fixtures/sources/*.json, so nothing here needs a provider key.
sources: { fixtures: true }
Key Default
id the folder’s name The tenant’s id, as in the app’s address. Pushing to a tenant you’re not in fails.
title the id The tenant’s name in the app.
timezone America/Sao_Paulo An IANA zone. Times are shown in it, and motions’ calendars resolve in it.
sources.fixtures false true answers built-in source calls from fixtures/sources/ instead of the providers.

The CRM connection, and how its properties map to Motions’ fields.

crm.yaml
# The built-in test CRM: Motions keeps these records itself and loads crm-seed.json into it on the first push.
# Property names are HubSpot's, so this field map carries over unchanged when you connect HubSpot.
system: memory
mode: read_write
companies:
core: { tax_id: cnpj, owner_id: hubspot_owner_id }
fields:
segment: { from: segmento, type: enum }
employees: { from: numberofemployees, type: number }
contacts:
core: { first_name: firstname, last_name: lastname, title: jobtitle, linkedin: hs_linkedin_url, owner_id: hubspot_owner_id }
deals:
core: { name: dealname, stage: dealstage, is_closed: hs_is_closed, owner_id: hubspot_owner_id }
notes:
core: { body: hs_note_body }
Key
system memory, hubspot memory is the built-in test CRM.
mode read_only, read_write read_only records approved writes as skipped. HubSpot only takes read_only for now.
<object>.core map Motions’ field → your property, for the fields below. Unmapped fields read the property of the same name.
<object>.fields map Your own fields: short name → { from, type, writable }.

Objects are companies, contacts, deals, owners and notes. Their core fields:

Object Core fields Usual HubSpot property
companies name, domain, tax_id, owner_id, country name, domain, your CNPJ property, hubspot_owner_id, country
contacts first_name, last_name, email, title, linkedin, owner_id firstname, lastname, email, jobtitle, hs_linkedin_url, hubspot_owner_id
deals name, stage, amount, is_closed, owner_id dealname, dealstage, amount, hs_is_closed, hubspot_owner_id
owners name, email none needed: HubSpot owners come with both
notes body, pinned hs_note_body

A custom field:

Key Default
from required The property’s internal name.
type none string, number, boolean, enum, string[] or date.
writable false Whether steps may propose new values for it.

Contacts’ first_name, last_name, email, title and linkedin, and notes’ body, are always writable. Companies’ core fields never are.

motions check fails when a motion proposes something the CRM can’t do, or system is unknown. A push fails for read_write on HubSpot, and for a crm-seed.json with any system but memory.

Records for the test CRM, loaded on the first push into an empty one. motions push --reseed replaces everything in it.

{
"owners": [{ "id": "o1", "properties": { "name": "Ana Souza", "email": "ana@orbita.example" } }],
"companies": [{ "id": "201", "properties": { "name": "Metalúrgica Aurora", "hubspot_owner_id": "o1" } }],
"contacts": [{ "id": "c1", "properties": { "firstname": "Rita" }, "company_ids": ["201"] }],
"deals": [{ "id": "d1", "properties": { "dealname": "Renewal", "hs_is_closed": false }, "company_ids": ["201"] }],
"notes": [{ "id": "n1", "properties": { "hs_note_body": "Met at the fair." }, "on": { "object": "companies", "id": "201" } }],
"lists": { "first-batch": ["201"] }
}
  • Properties use your CRM’s names, as crm.yaml maps them, so the same field map works on the seed and on HubSpot.
  • company_ids links contacts and deals to companies; on puts a note on a company or contact.
  • lists are what batches open on, by name.
  • Every id it links to must be in the seed.

Each file is a list of recorded answers. Files are read in name order, then entries in file order, and the first match wins.

fixtures/sources/rdap.json
[
{
"op": "rdap.lookup",
"request": { "domain": "aurorametais.example" },
"data": {
"domain": "aurorametais.example",
"registrant": "METALURGICA AURORA LTDA",
"registeredAt": "2009-03-17T00:00:00Z",
"changedAt": "2025-03-17T00:00:00Z",
"status": ["active"],
"server": "rdap.example"
}
},
{
"op": "rdap.lookup",
"request": { "domain": "serraverde.example" },
"data": {
"domain": "serraverde.example",
"registrant": "LATICINIOS SERRA VERDE S.A.",
"registeredAt": "2011-06-02T00:00:00Z",
"changedAt": "2025-06-02T00:00:00Z",
"status": ["active"],
"server": "rdap.example"
}
},
{
"op": "rdap.lookup",
"request": { "domain": "patiolog.example" },
"data": {
"domain": "patiolog.example",
"registrant": "GRUPO TRANSVIA PARTICIPACOES LTDA",
"registeredAt": "2018-09-20T00:00:00Z",
"changedAt": "2026-01-10T00:00:00Z",
"status": ["active"],
"server": "rdap.example"
}
},
{ "op": "rdap.lookup", "request": { "domain": "nuvemclara.example" }, "data": null }
]
Key
op The operation, such as rdap.lookup.
request Matches a call when every field it gives equals the call’s, recursively. {} matches any call to the op.
data What the call returns. null is “not found”.
error { code, message? }, to fail the call instead. Codes: not_found, bad_input, not_allowed, no_credit, budget, rate_limited, transient, config.
sequence A list of { data } or { error }, answered in turn to identical calls in a batch; the last one repeats.
note A comment for people.
package.json
{
"name": "orbita",
"private": true,
"type": "module",
"scripts": {
"check": "motions check",
"test": "motions test",
"typecheck": "tsc -p .",
"push": "motions push"
},
"devDependencies": {
"@motionskit/cli": "^0.1.0",
"@motionskit/sdk": "^0.1.0",
"typescript": "^5.9.3",
"vitest": "^5.1.0",
"zod": "^4.6.7"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["motions", "sources", ".motions/*.d.ts"]
}

.motions/*.d.ts is where motions check writes the types of your custom sources.

node_modules/
.motions/

Motions is built by Pruma. These docs describe what the platform does today; Limitations lists what it doesn't do yet. Open the app