Skip to main content

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

VarRequiredDefaultPurpose
TRIGGERS_NATS_URLYes at runtimenoneNATS cluster URL used for registration, broadcasts, and service runtime
TRIGGERS_NATS_CREDS_PATHNounset, no authPath to NKey credentials file
TRIGGERS_NATS_SOURCE_STREAMNotriggers-sources-<service>JetStream stream for inbound NATS events
TRIGGERS_SERVICE_VERSIONNoCI fallback chain (see below)Service version string for the manifest
TRIGGERS_DRAIN_TIMEOUTNo30sMax in-flight handler drain on shutdown
TRIGGERS_METRICS_ADDRNo:9090 (empty disables)Prometheus bind address
TRIGGERS_HTTP_ADDRNo:8081HTTP listen socket and fallback public base
TRIGGERS_HTTP_PUBLIC_BASE_URLNounsetOptional advertised HTTP base when it should differ from the bind-address fallback
TRIGGERS_KAFKA_BROKERSNounset; required if Kafka ingress is declaredComma-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_PARTITIONSNo1Partition count used only when the SDK creates the canonical Kafka ingress topic
TRIGGERS_KAFKA_INGRESS_TOPIC_REPLICATION_FACTORNo1Replication factor used only when the SDK creates the canonical Kafka ingress topic
TRIGGERS_KAFKA_INGRESS_TOPIC_RETENTION_MSNo604800000retention.ms used only when the SDK creates the canonical Kafka ingress topic; cleanup.policy=delete is always set alongside it
TRIGGERS_REDIS_URLNounset; lazy-resolved only when dedup or rate-limit is enabledFinal Redis URL for SDK capabilities; if set, it wins over all other Redis inputs and is exposed through wiring
TRIGGERS_REDIS_URL_SECRET_NAMENounsetKey Vault secret name for the full Redis URL; used only when TRIGGERS_REDIS_URL is empty
TRIGGERS_REDIS_HOSTNounsetLiteral Redis host[:port] or redis[s]://... base used to compose a URL when no full URL input is set
TRIGGERS_REDIS_HOST_SECRET_NAMENounsetKey Vault secret name for the Redis host[:port] or base URL; used only when TRIGGERS_REDIS_HOST is empty
TRIGGERS_REDIS_PASSWORDNounsetLiteral Redis password used when composing a URL
TRIGGERS_REDIS_PASSWORD_SECRET_NAMENounsetKey Vault secret name for the Redis password; used only when TRIGGERS_REDIS_PASSWORD is empty
TRIGGERS_REDIS_DBNounsetDB suffix applied when composing a URL from host/password inputs and the host/base URL has no DB path
KEY_VAULT_URLNounsetAzure Key Vault base URL used when any Redis *_SECRET_NAME input needs resolution
TRIGGERS_DEDUP_WINDOWNo30sDefault dedup window for enable_dedup() / EnableDedup()
TRIGGERS_RESULT_RECOVERY_ENABLEDNofalseBroadens action-result recovery to ordinary non-Timer deliveries; Timer-scoped recovery is automatic when durable_timer.v1 is advertised
TRIGGERS_RESULT_RECOVERY_TTLNo24hDuplicate-suppression horizon for stored action results; never a Durable Timer due time or schedule retention setting
TRIGGERS_MAX_IN_FLIGHTNo128Default NATS source max-ack-pending / in-flight limit
OTEL_EXPORTER_OTLP_ENDPOINTNounset, no tracing exportStandard OpenTelemetry env, not TRIGGERS_* prefixed
TRIGGERS_CEL_COST_LIMITNo1000000CEL evaluation cost limit in Go; Python currently has no matching cost budget
TRIGGERS_CEL_EVAL_DEADLINE_MSNo50CEL wall-clock deadline in Python; Go currently still uses a fixed 50ms evaluator timeout
TRIGGERS_MAX_PAYLOAD_BYTESNo1048576Payload decode size limit in Python; Go currently still uses a fixed 1 MiB envelope limit
TRIGGERS_MAX_PAYLOAD_DEPTHNo32Payload decode nesting limit in Python; Go currently still uses a fixed depth-32 envelope limit
TRIGGERS_CEL_PROGRAM_CACHENo10000Compiled 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():

  1. TRIGGERS_SERVICE_VERSION if set.
  2. Else ${BITBUCKET_BUILD_NUMBER}-${BITBUCKET_COMMIT[:8]} when Bitbucket CI vars are present. Go requires BITBUCKET_COMMIT to be at least 8 characters; Python slices whatever is available.
  3. Else dev.

Runtime wiring rules

  • svc.ingress(HTTP) / svc.Ingress(triggers.HTTP) always uses the canonical /v2/<service>/inbound path unless you opt into HTTP.at(...). The SDK publishes that path under TRIGGERS_HTTP_PUBLIC_BASE_URL or, when unset, TRIGGERS_HTTP_ADDR.
  • svc.ingress(KAFKA) / svc.Ingress(triggers.Kafka) requires TRIGGERS_KAFKA_BROKERS. The service declaration advertises only the canonical Kafka ingress topic triggers_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() and svc.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.v1 only 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=true broadens recovery to ordinary non-Timer deliveries and requires svc.enable_dedup() / svc.EnableDedup() plus a resolvable Redis backend. It is not required for Timer-scoped recovery. TRIGGERS_RESULT_RECOVERY_TTL defaults to 24h and 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, then TRIGGERS_REDIS_URL_SECRET_NAME, then host/password composition where literal TRIGGERS_REDIS_HOST or TRIGGERS_REDIS_PASSWORD beat their corresponding *_SECRET_NAME variants and TRIGGERS_REDIS_DB fills the DB suffix when needed.
  • Any Redis *_SECRET_NAME input requires KEY_VAULT_URL on the service process.
  • Successful resolution backfills TRIGGERS_REDIS_URL, and svc.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.

ServiceEnvironmentRedis host secretRedis password secretRedis DB
notify-senderspark-dev1spark-dev-redis-hostspark-dev-redis-password3
notify-senderspark-prod-mexico1spark-mexico1-prod-redis-hostspark-mexico1-prod-redis-password3
spark_bonusspark-dev1spark-dev-redis-hostspark-dev-redis-password5
spark_bonusspark-prod-mexico1spark-mexico1-prod-redis-hostspark-mexico1-prod-redis-password5

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.