Integrate a Service
The current SDK contract is simple:
- shape lives in code
- wiring lives in
TRIGGERS_*env - startup fails loudly when declared ingress or capabilities are missing their wiring
For both Go and Python, the authoring loop is the same:
- Create the service with its stable id.
- Declare ingress with
NATS,Kafka, and/orHTTP(HTTP.at(...)only when you need a custom route). - Register actions.
- Add
data_source(...),EnableDedup(), orEnableRateLimit()only when the service really needs them. - Run the lifecycle and let the SDK handle registration, flow broadcasts, dispatch, retries, DLQ, and heartbeats.
Canonical defaults
| Concern | Derived value |
|---|---|
| NATS ingress | triggers.v2.<service>.inbound |
| Kafka ingress | triggers_v2_<service>_inbound |
| HTTP route path | /v2/<service>/inbound |
| NATS source stream | triggers-sources-<service> |
| Dedup prefix | t2:<service>:dedup: |
| Rate-limit prefix | t2:<service>:ratelimit: |
The SDK derives those values from the service id. For HTTP and NATS, override only when the service truly has to interoperate with an existing external contract. Kafka ingress is stricter: service declarations stay canonical-only, and any non-canonical Kafka target belongs only in released flow configuration.
For Kafka ingress, the service declaration stays canonical and code-owned: Kafka advertises triggers_v2_<service>_inbound. Released flows may target a different Kafka topic, but that non-canonical target belongs only in released flow configuration, not in the service declaration. At runtime the SDK ensures only the canonical topic. If a released flow points at a different topic, the SDK checks that it already exists before switching to it. If that topic is missing, reconcile fails closed: the runtime closes the stale Kafka consumer instead of silently continuing on the old topic, so the service can stay up while Kafka ingestion is unavailable until the topic exists or the flow is corrected.
Shape vs wiring
Shape means:
- service id
- ingress declarations
- actions
- data sources
- capabilities
Wiring means:
TRIGGERS_NATS_URLTRIGGERS_KAFKA_BROKERSTRIGGERS_REDIS_URLor the SDK Redis secret-name inputsTRIGGERS_HTTP_ADDR- optional
TRIGGERS_HTTP_PUBLIC_BASE_URL TRIGGERS_SERVICE_VERSION
Env never adds or removes ingress. If code declares Kafka ingress and TRIGGERS_KAFKA_BROKERS is missing, the service does not quietly become NATS-only; it fails at startup. When Kafka ingress is declared and brokers are configured, the SDK uses that same broker connection to ensure the canonical service-in topic before opening the consumer.
Redis follows the same rule, but only for services that declare Redis-backed capabilities such as dedup or rate limit. The SDK resolves Redis lazily in this precedence order: TRIGGERS_REDIS_URL, then TRIGGERS_REDIS_URL_SECRET_NAME, then host/password inputs where literal values beat the corresponding *_SECRET_NAME vars and TRIGGERS_REDIS_DB fills the DB suffix when needed. Any *_SECRET_NAME path requires KEY_VAULT_URL.
Kafka ingress ownership
The normal path is:
- Declare
Kafkain code. - Let the SDK advertise
triggers_v2_<service>_inbound. - Set
TRIGGERS_KAFKA_BROKERS. - Allow the SDK to create that canonical topic, or pre-provision that same canonical topic out of band.
When the SDK creates the canonical topic, it uses explicit retention.ms plus cleanup.policy=delete.
Optional env overrides for the canonical topic shape:
| Var | Default |
|---|---|
TRIGGERS_KAFKA_INGRESS_TOPIC_PARTITIONS | 1 |
TRIGGERS_KAFKA_INGRESS_TOPIC_REPLICATION_FACTOR | 1 |
TRIGGERS_KAFKA_INGRESS_TOPIC_RETENTION_MS | 604800000 |
Invalid TRIGGERS_KAFKA_INGRESS_TOPIC_* values fail fast during config parsing.
Released flows may retarget Kafka delivery to a non-canonical topic. Those targets must already exist. The SDK only probes them for existence and does not create or reconfigure them. If a retargeted topic is missing, reconcile fails closed and Kafka ingestion stays unavailable until the topic exists or the flow is corrected.
HTTP ingress note
The default HTTP route path is /v2/<service>/inbound.
When you declare HTTP or triggers.HTTP, the SDK uses that canonical path automatically.
If you need a different route, HTTP.at("/custom/path") and triggers.HTTP.At("/custom/path") override it. The SDK then builds the manifest's absolute HTTP endpoint from:
TRIGGERS_HTTP_PUBLIC_BASE_URLwhen it is set- otherwise
TRIGGERS_HTTP_ADDR(default:8081)
Absolute URL overrides remain supported for interop, but the common repo pattern is bare HTTP with SDK-owned env resolution.
For single-host local development, TRIGGERS_HTTP_ADDR is usually enough. Use TRIGGERS_HTTP_PUBLIC_BASE_URL when the manifest should advertise a different host or scheme than the bind-address fallback.
Verify the integration
Before shipping a new service, check:
GET /api/v2/services/<service>shows the expected actions,service_sources, andtransports- the service is emitting heartbeats
- a released flow can route into each declared ingress you expect to support
- result-event metadata is present for any action that downstream nodes bind from