> 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.

# 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](https://www.dash0.com) 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:

```json
{
  "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](#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.

```shell-session
$ 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](#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:

```json
{
  "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`:

```shell-session
$ 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](https://docs.tessl.io/schedules/environments.md) 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](https://www.standardwebhooks.com), 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](https://docs.tessl.io/automations/stop-and-change-loops.md).

## 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](#send-events-from-a-run).
- **Repositories:** sources start runs only for projects whose repository is on GitHub.

## Related pages

- [Automations overview](https://docs.tessl.io/automations/overview.md): how loops, actions, triggers, and runs fit together
- [Configuration files](https://docs.tessl.io/reference/configuration.md#actions-triggers-and-schedules): the `triggers` block, `when`, and `concurrency`
- [Environments](https://docs.tessl.io/schedules/environments.md): where the `actionTrigger` setting lives
