Skip to content

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.

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.

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.

Two sources put a model’s judgment in a step:

  • llm.complete returns Claude’s text for a prompt. Give it a stable key, such as "fit_summary", so the cache and fixtures can tell calls apart.
  • typesafe.ask returns structured judgments: a choice among options with probabilities, a score on a scale, or a yes/no. The SDK’s choice, score and noul build 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 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