Triggers¶
All triggers share one envelope and one engine.
tenantis required: it selects the tenant's config, scripts and data.scriptis optional. If present, that script runs. If absent, the tenant's router picks one.versionis optional (defaultlatest).
Routing¶
When script is absent the tenant's router script picks one. See Router.
Direct Lambda invoke¶
Payload is the envelope. The response is:
The trigger is always invoke.
ALB → Fargate (HTTP)¶
curl -X POST http://host:8080/v1/acme/hello \
-H 'content-type: application/json' \
-H 'x-script-version: v3' \
-d '{"x": 1}'
The request body is the event; the response body is the script's return value. Health check:
GET /healthz. POST /v1/{tenant} (no script) uses the router.
| Status | Cause |
|---|---|
| 200 | success |
| 400 | invalid tenant/script/version name |
| 404 | script not found |
| 422 | script error, missing handler, no route, bad runtime or bad config |
| 504 | timeout |
| 500 | internal error |
SQS → Fargate¶
Set queue.url in the config. The message body is the envelope; omit script to have it routed. The worker long-polls in batches of 10 and
deletes a message only on success; failures return to the queue after the visibility timeout
and eventually land in the queue's dead-letter queue.
S3 notifications¶
Point an S3 event notification (s3:ObjectCreated:*) from each bucket at the queue. The
server recognises the notification and splits it into one invocation per object record:
- the tenant is the first path segment after
buckets.prefix, so one notification serves all tenants; - no script is named, so the tenant's router picks one;
- the event is
{ bucket, key, size, etag, event }, wherebucketis the logical name (In) andkeyis relative to the tenant's prefix, so both can go straight tonative.bucket; - other event types, folder markers and S3's test event are acknowledged and skipped.
If every record succeeds the message is deleted. If only some fail, just those records are sent
back as a new message (with a growing delay and an attempt counter) and the original is deleted,
so successful records are not run again. After 5 rounds the narrowed message is left alone and the
queue's redrive policy moves it to the DLQ. A record that cannot be mapped (unknown bucket, key
outside buckets.prefix) counts as failed, so it ends up in the DLQ instead of vanishing.
Delivery is at-least-once, so handlers must treat a missing object (get returns nil) as already
handled.
Deleting the object when done¶
A queue handler can ask the engine to delete objects after it returns successfully:
return { ok = true, delete = true } -- the event's own bucket + key
return { delete = { { bucket = "In", key = "a.xml" }, { bucket = "In", key = "b.xml" } } }
If the handler errors, nothing is deleted and the message is retried with the object still there.
Deletes go through the tenant's own grants, so the bucket needs the delete grant. At most 100
entries; delete is ignored for other triggers; a failed delete fails the invocation.
Choosing where to run¶
| Lambda | Fargate | |
|---|---|---|
| Best for | low or spiky volume, synchronous calls | steady load, queue consumers, warm caches |
| Cache | per execution environment | per task |
| Concurrency control | Lambda settings | your own (per-tenant caps are on the roadmap) |