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
Section titled “tenant.yaml”id: orbitatitle: Órbita Softwaretimezone: 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. |
crm.yaml
Section titled “crm.yaml”The CRM connection, and how its properties map to Motions’ fields.
# 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: memorymode: read_writecompanies: 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.
crm-seed.json
Section titled “crm-seed.json”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.yamlmaps them, so the same field map works on the seed and on HubSpot. company_idslinks contacts and deals to companies;onputs a note on a company or contact.listsare what batches open on, by name.- Every id it links to must be in the seed.
fixtures/sources/*.json
Section titled “fixtures/sources/*.json”Each file is a list of recorded answers. Files are read in name order, then entries in file order, and the first match wins.
[ { "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
Section titled “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
Section titled “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.
.gitignore
Section titled “.gitignore”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