Skip to content

Egresses ​

Egresses are Hantera's typed, ACL-gated primitive for outbound traffic. Where an ingress lets an external system call into one of your components, an egress lets your components reach out to an external system over HTTP.

What are Egresses? ​

An egress is a resource that binds a key to a component that performs an outbound call. Callers invoke it by key using the call filter — they never construct the HTTP request, set the destination, or hold the credentials themselves.

Egresses replace ad-hoc outbound calls made directly with the reactor http module. They give you:

  • A single typed call site — one component owns the request shape and the response shape.
  • A host allowlist by construction — the existence of the egress resource is the permission to reach that host. There is no separate allowed-hosts list to maintain.
  • A per-key ACL — only callers granted egresses/<key>:call can reach the destination.
  • Safe calls from rules — rules may call egresses marked idempotent: true inline, something the raw http module could never do.

The two pieces ​

An egress is always two things working together.

1. The egress component ​

A .hegress / .heg component that performs the call. It imports http (only in scope inside the egress runtime), declares the fields callers pass in as params, and returns the result via a single from. Credentials and base URLs are resolved inside the component (for example from the registry) so they never enter the calling reactor, rule, or job.

filtrera
//// components/egresses/createSession.heg

import 'http'

param configurationId: text
param toAddress: {
  name: text
  address1: text
  postalCode: text
  city: text
  country: text
}

let config = registry->'apps/my-app'

from
  let res = http {
    method = 'POST'
    url = $'{config->''baseUrl''}/checkouts'
    headers = {
      'Content-Type' -> 'application/json'
      'X-Api-Key' -> config->'clientSecret'
    }
    body = { configurationId = configurationId, toAddress = toAddress }
  }
  res.status match
    200 |> res.body
    |> { error = { code = $'http_{res.status}', message = res.body } }

2. The egress resource ​

A declaration that binds a key to the component, sets the idempotency gate, and carries the ACL. Create it through the HTTP API, apply it as a CLI manifest file, or declare it in an app's manifest:

PUT /resources/egresses/createSession
Content-Type: application/json
json
{
  "componentId": "components/egresses/createSession.heg",
  "idempotent": true,
  "acl": ["registry/apps/my-app:read"]
}

Calling an egress ​

Put the request payload on the left and the fully qualified egress key on the right:

filtrera
from
  { configurationId = 'cfg_123', toAddress = { ... } }
  call 'apps/my-app/createSession'

call returns the egress component program's result directly. There is no envelope — transport and business errors flow through the component's own result convention (by convention { error = { code, message } }), so callers handle success and failure with ordinary pattern matching.

Idempotency: where you can call from ​

Mark an egress idempotent: true only if repeating an identical call has the same observable effect as making it once.

idempotentCallable from rulesCallable from reactors & jobs
true✅✅
false❌✅

Rules are evaluated as part of synchronous request handling, so they may only make calls that are safe to retry. Non-idempotent egresses (e.g. "charge a card", "place an order") are restricted to reactors and jobs.

Response cache ​

Idempotent egresses can also opt into the response cache: a successful dispatch is stored in memory, and identical requests within a time-to-live are served from memory with zero network traffic. Because the cache sits below the component runtime, it deduplicates regardless of which rule, reactor, job, actor, or preview render produced the call — including calls from inside preview-rendered order pipelines, which cannot write to shared state any other way.

A single manifest field is the whole contract — a positive cacheTtlSeconds is the sole cache knob:

yaml
egresses:
  - id: calculate
    componentId: components/egresses/calculate.heg
    idempotent: true
    cacheTtlSeconds: 600        # positive = cache enabled; unset/zero/negative = disabled
    cacheKeyParameter: cacheKey # optional; see Cache keys
  • cacheTtlSeconds (seconds) — entries live at most this long, clamped to the deployment's Egress:Cache:MaxTtlSeconds (default 600). An over-max value is accepted and clamped, and reported as a warning signal in the registry so the clamping is visible.
  • A positive TTL requires idempotent: true — a cached response skips a call, which is a stronger claim than repeating one: the cache must never suppress a side effect the caller needed. h_ app pack and app install reject a positive TTL on a non-idempotent egress; an egress created directly through the API gets a warning signal and is never cached.

Cache keys ​

Two modes:

  1. Explicit (cacheKeyParameter): the parameter names a param on the egress component that carries the caller's cache key. Use this when the payload contains identity that is irrelevant to the response — the key can be deliberately coarser than the payload (e.g. a tax app passes a checksum of the tax-relevant inputs, so two different orders with the same inputs share an entry). An absent, nothing, or empty key bypasses the cache entirely: the call executes for real and nothing is stored — the escape hatch for forcing requests.

    filtrera
    // Egress component: declare the param; the body may or may not read it.
    param document: value
    param cacheKey = ''
    
    // Caller:
    from
      { document = document, cacheKey = currentChecksum }
      call 'apps/my-app/calculate'
  2. Automatic (no cacheKeyParameter): the key is a deterministic hash of the fully resolved request. Identical requests share an entry by construction — you cannot get the key wrong — at the cost of never deduplicating requests that differ in any payload detail.

Errors are never cached — neither thrown failures nor results shaped as the platform's { error = { code, message } } convention — so a failed call is retried for real on the next attempt. An active dev session bypasses the cache (both lookup and store) so you always trace real requests while developing.

Cache hits are counted separately per egress and shown in the portal monitoring view; the per-host statistics show the actual outbound network calls dropping to zero for cached dispatches.

Wrapping a raw URL with platform/http ​

If you don't need a custom typed component, bind the built-in platform/http component and pin the destination with a parameter override so callers cannot redirect the request:

yaml
egresses:
  - id: shopfront-webhook
    componentId: platform/http
    idempotent: true
    parameters:
      url: https://shop.example.com/hooks/order-created
filtrera
from
  {
    method = 'POST'
    headers = { 'Content-Type' -> 'application/json' }
    body = { orderId = order.id, total = order.total } asJson
  }
  call 'apps/my-app/shopfront-webhook'

Any param you pin under parameters is fixed by the resource and removed from the caller's responsibility — a clean way to lock down the URL while letting callers supply the method, headers, and body.

Sending over a connection ​

Egress components can also send messages over a connection — to RabbitMQ queues, Azure Service Bus entities, or Azure Event Hubs — with the send macro. Like HTTP egresses, the egress resource owns the destination: it holds the connection-use grant (connections/{id}:use) and the address, and callers never see either.

filtrera
//// components/egresses/publish.hegress
// One-way publish to a queue on the partner broker. `send` is total: transport
// failures come back as { sent = false, error = ... } instead of escaping the
// egress program. The platform stamps the ambient trace continuation automatically.

param eventType: text
param payload: value

from send {
  connection = 'apps/my-app/partner-rabbit'
  address = '/queues/orders-created'
  body = {
    eventType = eventType
    payload = payload
  }
  contentType = 'application/json'
  applicationProperties = {
    'eventType' -> eventType
  }
}

The egress resource declares the connection-use grant in its ACL:

yaml
egresses:
  - id: publish
    componentId: egresses/publish.hegress
    idempotent: false
    acl:
      - connections/apps/my-app/partner-rabbit:use

The send macro ​

FieldRequiredDescription
connectionYesThe connection id to send through.
addressYesThe destination address, interpreted by the connection's adapter.
bodyYesThe message body.
contentTypeNoContent type for the body (default application/json).
applicationPropertiesNoNamed application properties carried on the message.
messageId / correlationId / subject / replyToNoStandard message properties.

send is one-way and total: it never throws. The result is a record:

filtrera
let result = send { ... }

from result match
  { sent = true } |> ...
  { sent = false, error = { code, message } } |> ...

Sends carry the ambient trace, so a queue or stream ingress on the consuming side names this egress as its trace parent — the two request rows connect across the broker.

Access control ​

Calling and managing egresses use different permissions:

egresses/<key>:call   # invoke a specific egress
egresses:call         # invoke any egress (class-level wildcard)
egresses:read         # view egress configurations
egresses:write        # create, update, and delete egresses

Following least-privilege, call is never granted implicitly — not even to components in the same app as the egress. Every caller must explicitly list egresses/<key>:call in its own component ACL, whether the egress belongs to the same app or another app.

Monitoring ​

Each egress invocation is recorded. In the portal, System → Egresses shows per-key call volume, success rate, and latency over a time range, so you can spot a failing or slow downstream dependency at a glance.

Offline type-checking ​

The Hantera Development Studio type-checks both .hegress / .heg components and every call '<key>' against the egresses declared in the open app's h_app.yaml. No tenant connection is required — a manifest declaration is enough for full local validation, including the parameters each egress expects and the result type it returns.

See Also ​

© 2026 Hantera AB. Org. no.: 559242-9582