Skip to content

Sources

A step can’t open a network connection. Everything it learns from outside your CRM comes through a source: a named operation such as rdap.lookup or exa.search, called as ctx.sources.rdap.lookup({ domain }).

Going through sources is what makes a motion safe to run on hundreds of companies:

  • Declared. Each step lists the operations it may call in motion.yaml. A call to anything else is refused with “add it to the step’s sources”.
  • Cached. The same request within the operation’s cache time is answered from the cache, for free. Re-running a step costs nothing for what it already looked up.
  • Metered. Every call is recorded in the batch with its cost. Sources in the app totals the spend, and cap in motion.yaml stops a company or a batch before it spends more than you allow.
  • Keyed by Motions. Provider keys and secrets are stored in Motions. Step code never sees them.

Built-in sources come with the platform: domain registration, Brazilian company registries, web search, LinkedIn, contact enrichment, and language models. See the catalog for every operation and its price.

Custom sources are your own, written in TypeScript in the tenant repository under sources/. Each one declares the hosts it may reach and the secrets it needs, and runs sandboxed. Use one for an API the catalog doesn’t have, such as a client’s internal system. See Write a custom source.

Steps call both the same way. A custom source called brasilapi with an operation cnpj is ctx.sources.brasilapi.cnpj({ cnpj }), typed from its input schema.

A source call either returns data or throws. What happens next depends on why:

The call The step
found nothing (not_found) gets null, and decides what that means.
was rate limited, or hit a passing error (rate_limited, transient) is retried after a pause, up to the step’s retries.
would go over the cap (budget), or the provider is out of credit (no_credit) stops. The company is parked until someone acts.
wasn’t declared (not_allowed), was malformed (bad_input), has no key (config), or has no fixture (fixture_missing) fails, unless the step catches it.

soft() from the SDK turns a refused call into a value, for enrichment that’s nice to have: const found = await soft(ctx.sources.apollo.people_match(req)).

A tenant in fixture mode, sources: { fixtures: true } in tenant.yaml, never calls a built-in provider. Each built-in call is answered from the recorded answers in fixtures/sources/*.json:

[
{ "op": "rdap.lookup", "request": { "domain": "aurorametais.example" }, "data": { "registrant": "METALURGICA AURORA LTDA" } },
{ "op": "rdap.lookup", "request": { "domain": "nuvemclara.example" }, "data": null },
{ "op": "exa.search", "request": {}, "error": { "code": "rate_limited", "message": "slow down" } }
]
  • request matches when every field it lists equals the call’s. {} matches any request to that operation.
  • The first matching entry wins, so put specific entries before general ones.
  • An entry gives data, an error with a code and message, or a sequence of those, one per call, for testing retries.
  • A call with no matching entry fails with fixture_missing.

The same fixtures drive your tests: runStep from @motionskit/sdk/testing answers source calls from them. So a motion built in fixture mode has tests that pass, batches that run for free, and the same behaviour once it’s live.

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