A scaffold for building custom integrations against the RoboSystems public API.
Living example: robosystems-marketing-integration — a real integration built from this template that collects RFS's own marketing/usage metrics (GitHub, npm, PyPI, Docker Hub) and asserts them as a monthly metric series.
An integration lives in its own repository — this one — and speaks to the platform exclusively through the public API with an API key. It never runs inside the platform, so it survives every platform release, works identically against the managed cloud or a self-hosted deployment, and is yours to run anywhere: a cron job, a Lambda, a container, a GitHub Actions schedule.
Pick the lane that matches your data's nature (an integration can use more than one):
| Lane | What you send | What the platform enforces | Emitter |
|---|---|---|---|
| 1 — Ledger | Business events → the general ledger | Double-entry balance, capture-then-approve, closed-period gate, (source, external_id) idempotency |
emit/events.py |
| 2 — Semantic facts | A custom vocabulary + observed metric series | Typed concepts, presentation structure, per-period replace, provenance | emit/metrics.py |
| 3 — Raw graph | Parquet/CSV files → staging → graph | Per-graph schema, bulk ingestion pipeline | emit/graph.py |
- Lane 1 is for data that is accounting: invoices, payments, payroll from a system the platform has no adapter for. Events land in an inbox (
captured), an operator approves, handlers derive the GL entries. - Lane 2 is for data that is facts but not bookkeeping: marketing counts, usage numbers, operational KPIs. Author your vocabulary once (
create-taxonomy-block), then assert observed values per period (assert-metrics). The platform renders the series everywhere — envelopes, charts, fact grids, GraphQL, MCP — with no further work. - Lane 3 is for graph-shaped domain data: upload files, stage, materialize. Combined with the per-graph schema operations you can stand up a complete custom knowledge graph.
- Click Use this template and create your integration repo (typically private).
- Clone it, then:
just venv # environment + dependencies + git hooks (.env provisioned from .env.example)
# fill in API key + graph id in .env
just run # collect → transform → emit- Replace the stubs in
src/integration/collect.pyandtransform.pywith your source's extract/transform, and wire the emitter(s) for your lane inmain.py.
Day-to-day commands mirror the other RoboSystems Python repos: just test, just test-all (tests + format + lint + typecheck — the CI gate), just lint, just format, just typecheck, just create-feature <type> <name>.
src/integration/
config.py # env-driven settings (.env supported)
client.py # robosystems-client SDK wired with API-key auth,
# plus envelope unwrapping + a raw-op escape hatch
collect.py # YOUR extract — pull from your source system
transform.py # YOUR transform — shape raw data for the lane you use
emit/
events.py # lane 1: create-event-block
metrics.py # lane 2: create-taxonomy-block + assert-metrics
graph.py # lane 3: create-file-upload → ingest-file → materialize
main.py # collect → transform → emit
The split is deliberate: you own extract and transform (they're specific to your source); the platform owns validation and load (the operations behind the API). Credentials for your source system stay on your side — the platform never holds them.
- Idempotency: lane-1 events carry
(source, external_id)— re-sending the same event is a no-op, so retries are always safe. Operations also accept anIdempotency-Keyheader (the client sends one when you pass it). - Raw history is yours: keep your raw pulls (e.g. daily JSON snapshots) in your own storage; send the platform the rolled-up series at your reporting cadence. Backfill is one loop over your history.
- One source name per integration: your integration's registered source name is its identity stamp on everything it writes.
An integration is a small program that runs on a schedule — the contract is env vars in, API calls out — so the runtime is swappable. The ladder, in increasing weight:
- GitHub Actions (the default, included):
.github/workflows/run.ymlruns the integration on a cron with zero infrastructure. Setsecrets.ROBOSYSTEMS_API_KEYplusvars.ROBOSYSTEMS_GRAPH_ID/vars.INTEGRATION_SOURCE_NAMEin the repo settings and it's deployed. Standard runners give 4 vCPU / 16 GB and a 6-hour cap — ample for API-pull collectors. - GitHub Actions, larger runner: heavy backfills or transforms get real memory/CPU with a one-line
runs-onchange (e.g.ubuntu-latest-8-cores) — still zero infrastructure. - ECS Fargate scheduled task: full resource control and CloudWatch visibility when an integration outgrows runners — EventBridge Scheduler →
RunTask, no service or load balancer, public subnet with egress only (the integration never needs to be inside a VPC; the API is the boundary). This template doesn't ship the CloudFormation for it — bring it when you graduate.
Secrets (API key, source credentials) belong in your runtime's secret store — repo secrets, or the task's secret manager — never in the repo.
The robosystems-client Python SDK is how the emitters interface with the platform — typed request/response models and one generated function per operation (robosystems_client.api.*), regenerated from the live OpenAPI spec. Brand-new operations occasionally land on the API before the SDK's next regeneration; IntegrationClient.raw_operation reaches those through the same authenticated client until the typed function exists.
pyproject.toml pins robosystems-client>=1,<2, and that range is safe because this template defines the SDK's stable surface. Every symbol the emitters import — AuthenticatedClient, types.UNSET, the emit operations (create_file_upload, ingest_file, materialize, create_event_block, assert_metrics, create_taxonomy_block) and their request models — is frozen for the life of 1.x. Breaking any of it costs the SDK a major version.
The rest of robosystems_client.api.* carries no such promise. It tracks the API surface and moves with it on minor releases, so operations you reach for beyond the emitters above — including anything through raw_operation — can be renamed or removed without a major. If you build on one and want it frozen, the way to get that is to add it to this template's emitters, not to narrow the pin.
This project is licensed under the MIT License - see the LICENSE file for details.
MIT © 2026 RFS LLC