For AI agents: the complete documentation index is available at https://docs.tessl.io/llms.txt, the full documentation bundle is available at https://docs.tessl.io/llms-full.txt, and this page is available as Markdown at https://docs.tessl.io/automations/event-sources.md.

Event sources

Info

Takeaways:

  • An event source is a named inbox on a project. Events sent to it start the project's loops, the way a GitHub or Linear event does.
  • An outside service posts a webhook to the source's URL with the source's secret in the X-Tessl-Source-Secret header. Your code and your runs post named events to the API with a Tessl key.
  • A trigger listens on source:<name>.webhook for webhooks and on source:<name>.<event> for named events.
  • A source accepts about 60 events a minute and bodies up to 1 MB. Sources start runs only for projects whose repository is on GitHub.

GitHub and Linear events cover work that starts in a repository or a ticket. Much of the work you want a loop to pick up starts somewhere else: an alert in your monitoring tool, a deploy finishing, a message in your own system, or a run deciding that another loop should take over. An event source gives those senders a way to start a loop.

Before you start

  • A Tessl project for a repository on GitHub. A source on a project whose repository isn't on GitHub accepts events, but starts no runs.
  • The factory enabled for your organization. The Event sources section appears only then.
  • To add, rotate, or delete a source, permission to manage integrations in the project's workspace, the same permission that connects GitHub.

How a source works

A source belongs to one project and has a name, such as dash0 or deploys. A name starts with a letter and holds only letters, digits, and _, up to 64 characters. Events sent to a source reach that project alone, and a run they start runs as the project's automation.

Events arrive at a source in two ways, and each has its own event name:

  • Webhooks. An outside service posts to the source's webhook URL. Every webhook arrives as source:<name>.webhook, with the request body as the payload.
  • Named events. Code holding a Tessl API key, or a run holding its own run key, posts an event with a name you choose. It arrives as source:<name>.<event>. For example, the event deploy.finished sent to the deploys source is source:deploys.deploy.finished.

A trigger in the project's tessl.json names the event in on, filters on the payload with when, and passes payload values to the action with {{ event.<path> }}, as for any other provider. In a trigger, event. is the payload you sent: a webhook's body, or a named event's payload. An event whose payload lacks a field the trigger's inputs, instructions, or concurrency keys read starts no run.

Add a source

  1. In the web app, open the project, then Settings.
  2. Under Event sources, enter a name, such as dash0, and choose Add source.
  3. Copy the Webhook URL and the webhook secret. The secret is shown once. You can't see it again.

The source's row shows when it last accepted an event. Rotate secret issues a new secret and shows it once. The old secret is refused from then on, so update every sender that uses it. Delete removes the source, which then refuses every event and webhook. A source you add again under the same name gets a new webhook URL.

Connect a webhook service

Any service that can send a custom HTTP header can post to a source:

  • URL: the source's webhook URL, https://api.tessl.io/experimental/webhooks/sources/<source id>. The web app shows it with your region's host.
  • Method: POST.
  • Header: X-Tessl-Source-Secret set to the source's secret.
  • Body: JSON (Content-Type: application/json) or plain text (text/plain), up to 1 MB. A JSON body becomes the payload as parsed. A text body is kept as sent, as a string that when can't read fields from. Other formats, such as XML or form-encoded bodies, are refused with 415.

The sender must be able to set that header. A service that can only sign its request body, with no custom header, can't call a source directly yet.

The source answers 202 with {"message": "Webhook received"} whether or not a run started, and 401 for a missing or wrong secret. To see whether a webhook started a run, open the loop's page.

Example: Dash0 alerts

This example starts a triage loop each time a Dash0 check starts failing.

  1. Add a source named dash0, and copy its webhook URL and secret.
  2. In Dash0, create a notification channel of type Webhook. Set URL to the webhook URL, and under Additional HTTP Headers add X-Tessl-Source-Secret with the secret.
  3. Route the alerts you want to that channel in your Dash0 check rules.
  4. Add a trigger to tessl.json and push it to the default branch:
{
  "triggers": {
    "dash0-alert": {
      "on": ["source:dash0.webhook"],
      "when": { "event.type": "alert.ongoing" },
      "action": {
        "skill": "engteam/alert-triage",
        "instructions": "Triage the Dash0 issue {{ event.data.issue.id }}."
      },
      "concurrency": {
        "keys": ["dash0-{{ event.data.issue.id }}"],
        "mode": "skip"
      }
    }
  }
}

Dash0 sends the alert state in type, such as alert.ongoing or alert.resolved, and the issue under data.issue. The when condition starts runs only for alerts that are ongoing. Dash0 documents no delivery id header, so the concurrency key on the Dash0 issue id with mode: skip keeps a repeated notification for the same issue from starting a second run while the first is waiting or running. See Delivery ids and retries.

Send events from your code

Post a named event to POST /v1/projects/{projectId}/sources/{name}/events with a Tessl API key that can trigger actions in the project's workspace. tessl project list shows the project's ID.

$ curl -X POST "https://api.tessl.io/v1/projects/$PROJECT_ID/sources/deploys/events" \
    -H "Authorization: Bearer $TESSL_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "event": "deploy.finished",
      "deliveryId": "deploy-4821",
      "payload": { "service": "checkout", "environment": "production" }
    }'

For an organization on the EU region, use https://api.eu.tessl.io.

The body has 3 fields:

  • event (string, required): the event's name. It starts with a letter and holds only letters, digits, _, and ., up to 256 characters.
  • deliveryId (string, required): your id for this event, 1 to 128 characters. See Delivery ids and retries.
  • payload (JSON object, optional): the data a trigger reads as event.<path>.

A 202 answer carries outcome: accepted when an active trigger listens for the event and its when conditions match, or when the source already received the same deliveryId, and ignored otherwise, including when a trigger listens but its when doesn't match. Nothing is kept for an ignored event, so its deliveryId can be sent again.

A trigger for this event:

{
  "triggers": {
    "after-deploy": {
      "on": ["source:deploys.deploy.finished"],
      "when": { "event.environment": "production" },
      "action": {
        "use": "smoke-check",
        "inputs": { "SERVICE": "{{ event.service }}" }
      },
      "concurrency": { "keys": ["deploy-{{ event.service }}"] }
    }
  }
}

Send events from a run

A run can start other loops in its project by sending an event with its own run key. Every run has the key in TESSL_TOKEN, its project's ID in TESSL_PROJECT_ID, and the API host in TESSL_API_BASE_URL:

$ curl -X POST "$TESSL_API_BASE_URL/v1/projects/$TESSL_PROJECT_ID/sources/handoff/events" \
    -H "Authorization: Bearer $TESSL_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "event": "review.requested", "deliveryId": "'"$TESSL_LAUNCH_RUN_ID"'", "payload": { "branch": "fix-login" } }'

A run key can send only:

  • to a source of the run's own project,
  • while the run is still running,
  • when the run's environment allows loops to start other loops in the project, the actionTrigger: project setting. That setting lets the run both trigger its own project's declared actions and send events to its own project's sources.

Otherwise the request is refused with 403 and the reason. A run that an event from a run starts records the sending run as its parent, so its cost is counted with the run that started it.

Nothing limits how many runs can start one another this way. A loop whose runs send the event that starts it runs until something stops it, so give such a chain an end condition in its when or its skill. The per-source rate limit is the only brake.

Delivery ids and retries

A source records each event that a trigger listens for and whose when conditions match, under its delivery id. An event with a delivery id the source already recorded starts nothing again, so a sender can retry safely. The same id sent to another source is a different event.

  • Named events always carry a deliveryId. Use one that stays the same when you retry the same event.
  • Webhooks take their delivery id from the webhook-id header, used by senders that follow Standard Webhooks, or else from the Idempotency-Key header, up to 128 characters. Without either, every webhook is a new delivery, so a sender's retry starts another run. For such senders, declare concurrency.keys on a value that identifies the event, with "mode": "skip", as the Dash0 example does. With skip, an event is dropped while another run on the same keys is waiting or running.

An event that no active trigger matches isn't recorded, whether no trigger listens for it or it fails the when of every trigger that does. A later event with the same delivery id is matched afresh and can start a run. A webhook that arrives while its loops are paused is dropped and not recorded: unpausing the loops doesn't replay it, and if the sender resends it after you unpause, the resend starts a run. See Stop, remove, and move loops.

Limits

  • Rate: a source accepts about 60 events a minute, webhooks and named events together. Past that, it answers 429. Failed webhook attempts, such as ones with a wrong secret, count toward the limit.
  • Body size: up to 1 MB. Past that, the source answers 413.
  • Chains: no limit on how many runs start one another. See Send events from a run.
  • Repositories: sources start runs only for projects whose repository is on GitHub.