Environment variables
The SDK snapshots runtime wiring from TRIGGERS_* env at service construction time, then lazily resolves Redis-backed inputs on wiring and startup surfaces only when those capabilities are declared. Service shape still comes from code. Use svc.wiring() in Python or svc.Wiring() in Go when app code needs the resolved snapshot instead of re-reading env vars.
Catalog
| Var | Required | Default | Purpose |
|---|---|---|---|
TRIGGERS_NATS_URL | Yes at runtime | none | NATS cluster URL used for registration, broadcasts, and service runtime |
TRIGGERS_NATS_CREDS_PATH | No | unset, no auth | Path to NKey credentials file |
TRIGGERS_NATS_SOURCE_STREAM | No | triggers-sources-<service> | JetStream stream for inbound NATS events |
TRIGGERS_SERVICE_VERSION | No | CI fallback chain (see below) | Service version string for the manifest |
TRIGGERS_DRAIN_TIMEOUT | No | 30s | Max in-flight handler drain on shutdown |
TRIGGERS_METRICS_ADDR | No | :9090 (empty disables) | Prometheus bind address |
TRIGGERS_HTTP_ADDR | No | :8081 | HTTP listen socket and fallback public base |
TRIGGERS_HTTP_PUBLIC_BASE_URL | No | unset | Optional advertised HTTP base when it should differ from the bind-address fallback |
TRIGGERS_KAFKA_BROKERS | No | unset; required if Kafka ingress is declared | Comma-separated broker list; the SDK uses it for Kafka ingress consumers, to ensure the canonical ingress topic, and to probe released-flow Kafka targets |
TRIGGERS_KAFKA_INGRESS_TOPIC_PARTITIONS | No | 1 | Partition count used only when the SDK creates the canonical Kafka ingress topic |
TRIGGERS_KAFKA_INGRESS_TOPIC_REPLICATION_FACTOR | No | 1 | Replication factor used only when the SDK creates the canonical Kafka ingress topic |
TRIGGERS_KAFKA_INGRESS_TOPIC_RETENTION_MS | No | 604800000 | retention.ms used only when the SDK creates the canonical Kafka ingress topic; cleanup.policy=delete is always set alongside it |
TRIGGERS_REDIS_URL | No | unset; lazy-resolved only when dedup or rate-limit is enabled | Final Redis URL for SDK capabilities; if set, it wins over all other Redis inputs and is exposed through wiring |
TRIGGERS_REDIS_URL_SECRET_NAME | No | unset | Key Vault secret name for the full Redis URL; used only when TRIGGERS_REDIS_URL is empty |
TRIGGERS_REDIS_HOST | No | unset | Literal Redis host[:port] or redis[s]://... base used to compose a URL when no full URL input is set |
TRIGGERS_REDIS_HOST_SECRET_NAME | No | unset | Key Vault secret name for the Redis host[:port] or base URL; used only when TRIGGERS_REDIS_HOST is empty |
TRIGGERS_REDIS_PASSWORD | No | unset | Literal Redis password used when composing a URL |
TRIGGERS_REDIS_PASSWORD_SECRET_NAME | No | unset | Key Vault secret name for the Redis password; used only when TRIGGERS_REDIS_PASSWORD is empty |
TRIGGERS_REDIS_DB | No | unset | DB suffix applied when composing a URL from host/password inputs and the host/base URL has no DB path |
KEY_VAULT_URL | No | unset | Azure Key Vault base URL used when any Redis *_SECRET_NAME input needs resolution |
TRIGGERS_DEDUP_WINDOW | No | 30s | Default dedup window for enable_dedup() / EnableDedup() |
TRIGGERS_RESULT_RECOVERY_ENABLED | No | false | Broadens action-result recovery to ordinary non-Timer deliveries; Timer-scoped recovery is automatic when durable_timer.v1 is advertised |
TRIGGERS_RESULT_RECOVERY_TTL | No | 24h | Duplicate-suppression horizon for stored action results; never a Durable Timer due time or schedule retention setting |
TRIGGERS_MAX_IN_FLIGHT | No | 128 | Default NATS source max-ack-pending / in-flight limit |
OTEL_EXPORTER_OTLP_ENDPOINT | No | unset, no tracing export | Standard OpenTelemetry env, not TRIGGERS_* prefixed |
TRIGGERS_CEL_COST_LIMIT | No | 1000000 | CEL evaluation cost limit in Go; Python currently has no matching cost budget |
TRIGGERS_CEL_EVAL_DEADLINE_MS | No | 50 | CEL wall-clock deadline in Python; Go currently still uses a fixed 50ms evaluator timeout |
TRIGGERS_MAX_PAYLOAD_BYTES | No | 1048576 | Payload decode size limit in Python; Go currently still uses a fixed 1 MiB envelope limit |
TRIGGERS_MAX_PAYLOAD_DEPTH | No | 32 | Payload decode nesting limit in Python; Go currently still uses a fixed depth-32 envelope limit |
TRIGGERS_CEL_PROGRAM_CACHE | No | 10000 | Compiled CEL program cache capacity per evaluator/cache instance |
Go and Python still fall back to bare NATS_URL / NATS_CREDS_PATH when the TRIGGERS_ names are unset. The table above lists the preferred names.
TRIGGERS_SERVICE_VERSION fallback chain
Resolved in Python by triggers.config.resolve_service_version() and in Go by resolveServiceVersion():
TRIGGERS_SERVICE_VERSIONif set.- Else
${BITBUCKET_BUILD_NUMBER}-${BITBUCKET_COMMIT[:8]}when Bitbucket CI vars are present. Go requiresBITBUCKET_COMMITto be at least 8 characters; Python slices whatever is available. - Else
dev.
Runtime wiring rules
svc.ingress(HTTP)/svc.Ingress(triggers.HTTP)always uses the canonical/v2/<service>/inboundpath unless you opt intoHTTP.at(...). The SDK publishes that path underTRIGGERS_HTTP_PUBLIC_BASE_URLor, when unset,TRIGGERS_HTTP_ADDR.svc.ingress(KAFKA)/svc.Ingress(triggers.Kafka)requiresTRIGGERS_KAFKA_BROKERS. The service declaration advertises only the canonical Kafka ingress topictriggers_v2_<service>_inbound. At runtime, the SDK ensures only that canonical topic before starting the consumer. If a released flow targets a different Kafka topic, the SDK probes it for existence only. When that retarget points at a missing topic, 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 remains unavailable until the topic exists or the flow is corrected.svc.enable_dedup()/svc.EnableDedup()andsvc.enable_rate_limit()/svc.EnableRateLimit()mark Redis as required, but the SDK only resolves Redis lazily when one of those Redis-backed capabilities is declared and wiring or runtime startup needs it.- Durable Timer v1 is optional. An SDK advertises
durable_timer.v1only when ordinary NATS, SDK dedup, and Redis are present. The capability automatically enables SDK-owned action-result recovery for structurally valid private Timer handoffs. It does not enable Notify or Durable Timer and adds no Timer configuration to a business service. TRIGGERS_RESULT_RECOVERY_ENABLED=truebroadens recovery to ordinary non-Timer deliveries and requiressvc.enable_dedup()/svc.EnableDedup()plus a resolvable Redis backend. It is not required for Timer-scoped recovery.TRIGGERS_RESULT_RECOVERY_TTLdefaults to24hand is a duplicate-suppression horizon, not a Timer due time. Durable Timer does not change action arguments, action results, public HTTP handlers, manifest action schemas, or ordinary non-Timer behavior unless this broad option is enabled.- Durable Timer commands use the service's existing plain NATS connection. The v1 deployment is a shared plain NATS 2.14 trusted bus with no ACL, TLS, credentials, signatures, or second Timer connection.
- Redis resolution precedence is:
TRIGGERS_REDIS_URL, thenTRIGGERS_REDIS_URL_SECRET_NAME, then host/password composition where literalTRIGGERS_REDIS_HOSTorTRIGGERS_REDIS_PASSWORDbeat their corresponding*_SECRET_NAMEvariants andTRIGGERS_REDIS_DBfills the DB suffix when needed. - Any Redis
*_SECRET_NAMEinput requiresKEY_VAULT_URLon the service process. - Successful resolution backfills
TRIGGERS_REDIS_URL, andsvc.wiring()/svc.Wiring()exposes the resolved snapshot, including parsed Redis fields and HTTP base/ingress URLs.
For simple local runs, TRIGGERS_HTTP_ADDR is usually enough on its own. Set TRIGGERS_HTTP_PUBLIC_BASE_URL only when the advertised URL must differ from the bind-address fallback, such as Kubernetes service DNS or compose-internal hostnames.
Durable Timer v1
The complete cross-process configuration, including Notify resume identity, Durable Timer service variables, and /health versus /ready behavior, is documented in triggers-sdk-go/docs/durable-timer-v1-configuration.md.
Durable Timer control state is an unsigned, SDK-private JSON context carried only on trusted internal NATS deliveries. It is paired with X-Triggers-Execution-Id and X-Triggers-Durable-Timer-Payload-SHA256, the lowercase SHA-256 of the exact current-hop bytes. The context's snapshot_sha256 remains the immutable original Start snapshot hash; it is not compared with a later mapped or action result. The private triplet is never passed to action handlers, action inputs, action results, public HTTP, Kafka, or output-topic transports.
Durable Timer is enabled for an environment through TRIGGERS_DURABLE_TIMER_ENABLED on Notify Triggers. Business SDK services do not set a Timer flow list. A Timer-lineage service-in target must exactly match an active manifest NATS service_sources[].default.
For Kafka ingress, the SDK creates only the canonical topic triggers_v2_<service>_inbound when it is missing. That create request always sets cleanup.policy=delete plus retention.ms from TRIGGERS_KAFKA_INGRESS_TOPIC_RETENTION_MS, and it uses TRIGGERS_KAFKA_INGRESS_TOPIC_PARTITIONS plus TRIGGERS_KAFKA_INGRESS_TOPIC_REPLICATION_FACTOR for the topic shape. The defaults are 1, 1, and 604800000, and invalid TRIGGERS_KAFKA_INGRESS_TOPIC_* values fail fast during config parsing. Non-canonical Kafka targets belong in released flow configuration only, not in service declarations. Those topics must already exist, and a missing retarget fails closed by stopping Kafka ingestion rather than silently reusing the old topic.
K8s notes
Keep terminationGracePeriodSeconds above the full shutdown budget, not just TRIGGERS_DRAIN_TIMEOUT. The current production example uses a 10s preStop delay before SIGTERM, TRIGGERS_DRAIN_TIMEOUT=30s, and terminationGracePeriodSeconds: 45, leaving 5s of slack for shutdown bookkeeping and the final shutting_down=true heartbeat.
When dedup or rate limiting needs Redis in Kubernetes, pass the official SDK inputs directly into the service container. The SDK now owns Redis resolution, so services should not re-implement that contract in their own bootstrap code. You can provide a direct TRIGGERS_REDIS_URL, a TRIGGERS_REDIS_URL_SECRET_NAME, or host/password inputs plus optional TRIGGERS_REDIS_DB, and the SDK will resolve and backfill TRIGGERS_REDIS_URL only when a Redis-backed capability actually needs it.
The current repo preference is host/password composition backed by Key Vault:
envFromSecrets:
- azure-app
environment:
TRIGGERS_REDIS_HOST_SECRET_NAME: "<env-specific-redis-host-secret>"
TRIGGERS_REDIS_PASSWORD_SECRET_NAME: "<env-specific-redis-password-secret>"
TRIGGERS_REDIS_DB: "<service-specific-db>"
In this repo, azure-app typically injects the Azure credential env plus KEY_VAULT_URL, so the SDK can resolve secret-name inputs without per-service Redis bootstrap code.
Current repo examples
These examples document values already used in env/ today. They are good templates for matching environments, but they are not blanket defaults for every new service.
| Service | Environment | Redis host secret | Redis password secret | Redis DB |
|---|---|---|---|---|
notify-sender | spark-dev1 | spark-dev-redis-host | spark-dev-redis-password | 3 |
notify-sender | spark-prod-mexico1 | spark-mexico1-prod-redis-host | spark-mexico1-prod-redis-password | 3 |
spark_bonus | spark-dev1 | spark-dev-redis-host | spark-dev-redis-password | 5 |
spark_bonus | spark-prod-mexico1 | spark-mexico1-prod-redis-host | spark-mexico1-prod-redis-password | 5 |
Use those values only when you are wiring the same service in the same environment family. For a different service, treat TRIGGERS_REDIS_DB as an explicit ownership decision, not something to copy blindly.