Use sources
Everything a step learns from outside the CRM comes through ctx.sources. This page is about calling them. For why they work this way, see Sources; for every operation and its price, the catalog.
Declare, then call
Section titled “Declare, then call”List the operations in the step’s sources:
- id: identity run: steps/identity.ts sources: [cnpj.match, rdap.lookup] cap: { per_company_usd: 0.03 }Then call them, typed:
const match = await ctx.sources.cnpj.match({ name: company.core.name, domain: company.core.domain ?? undefined });if (match.status === "unresolved") return hold("no_cnpj");ctx.sources.call("brasilapi.cnpj", { cnpj }) is the same thing by name. Custom sources are typed too, once motions check has generated .motions/sources.d.ts; see Write a custom source.
Errors
Section titled “Errors”A call either returns its data or throws. Most of the time, let it throw: Motions knows what each error means.
| Error | What Motions does |
|---|---|
not_found |
Nothing throws: the call returns null. |
rate_limited, transient |
Retries the step after a pause, up to its retries. |
budget, no_credit |
Parks the company until someone acts. |
not_allowed, bad_input, config, fixture_missing |
Fails the step. |
For lookups that are nice to have but shouldn’t fail the step, wrap them in soft(). A refused call becomes a value; budget, retry and question signals still go through:
import { soft } from "@motionskit/sdk";
const person = await soft(ctx.sources.apollo.people_match({ first_name, last_name, domain }));const email = person.ok ? person.value?.email ?? null : null;Every call has a price, recorded with the batch. Two settings keep it in check:
cap.per_company_usd: the most one company may spend in this step. Before each call, Motions checks the call’s estimated price against what’s left; a call that would go over isn’t made, and the company is parked with “budget”.cap.per_batch_usd: the same for the whole batch, across all companies in this step.
Cached answers are free and don’t count. Set caps from the catalog’s prices and how many calls a company needs, with some room.
Language models
Section titled “Language models”Two sources put a model’s judgment in a step:
llm.completereturns Claude’s text for a prompt. Give it a stablekey, such as"fit_summary", so the cache and fixtures can tell calls apart.typesafe.askreturns structured judgments: a choice among options with probabilities, a score on a scale, or a yes/no. The SDK’schoice,scoreandnoulbuild the questions:
import { choice, defineStep, score } from "@motionskit/sdk";
const judged = await ctx.sources.typesafe.ask({ state: { name: company.core.name, site: signals.summary }, questions: { segment: choice("Which segment does this company sell to?", { smb: "Small businesses", mid: "Mid-market", ent: "Enterprises" }), fit: score("How well does it fit our ICP?", ["poor", "weak", "good", "strong"]), },});if (judged.fit.confidence < 0.6) { // not sure: ask a person}In fixture mode and tests
Section titled “In fixture mode and tests”In fixture mode, and in runStep, every call is answered from fixtures/sources/*.json. A call with no matching fixture fails with “no fixture matches <op> <request>; add one to fixtures/sources”, so add one for each case you test. 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