# Events

Every event your organization’s audit log records and event streaming can send.

The name, such as `workflow.publish`, is what the audit log’s **Event** filter, the CSV and a streamed event’s `type` carry. Each event reaches both the audit log and streaming, unless its name says **Streaming only**.

**Where events go**

Run results and an expired connection are sent by streaming only. They come for production runs only. A Sub-flow’s run sends no end of its own: it is part of the run that called it.

**High volume** events are left unticked unless you tick them.

A person’s own profile changes and account deletion, and the installation’s own administration, are only in [Platform › Audit log](https://docs.gorunner.io/self-host/platform-audit-log).

| Event | What it records |
| --- | --- |
| **Sign-in** |   |
| `auth.login` | **Signed in**. A person signed in by emailed link, typed code, second factor, recovery code or single sign-on, or on a self-hosted installation without mail by password or reset link (`metadata.method`). |
| `auth.signup` | **Signed up**. A new account was created by its first sign-in. |
| `auth.login.failed` | **Sign-in failed**. A sign-in link, code or second factor was refused. Counted per address and reason, at most a few rows a minute. |
| `auth.link.requested` | **Sign-in link requested**. Someone asked for a sign-in or confirmation link to be emailed. |
| `auth.stepup` | **Identity confirmed**. A signed-in person confirmed it was them before a sensitive change. |
| `auth.logout` | **Signed out**. A person signed out of a session. |
| `auth.password.lockout` | **Sign-ins paused**. Five wrong passwords for an address in a row: its password sign-ins stop for 15 minutes (self-hosted, no mail). |
| `embed.session.start` | **Embedded session started**. A partner's customer (or a platform admin previewing as one) opened the embedded product. |
| **Workflows** |   |
| `workflow.create` | **Workflow created**. A workflow was created: by a person, a token, or Keel (actor type keel). |
| `workflow.save` High volume | **Workflow saved**. A workflow's draft was saved (the editor saves as you work; Keel saves a proposal). |
| `workflow.import` | **Workflow imported**. A workflow was imported from an exported workflow file. |
| `workflow.duplicate` | **Workflow duplicated**. A workflow was copied into a new draft. |
| `workflow.publish` | **Workflow published**. A workflow's draft went live in production. |
| `workflow.unpublish` | **Workflow unpublished**. A workflow was taken out of production. |
| `workflow.delete` | **Workflow deleted**. A workflow was deleted. |
| `workflow.move` | **Workflow moved**. A workflow was filed into (or out of) a folder. |
| `workflow.tag` | **Workflow tags changed**. A workflow's tags were changed. |
| `workflow.restore` | **Version restored**. An earlier version was restored into a workflow's draft. |
| `workflow.variables.update` | **Variables changed**. A workflow's variables were changed (names only, never values). |
| `workflow.webhook.rotate` | **Webhook address replaced**. A workflow's public webhook address was retired and a new one issued. |
| `folder.create` | **Folder created**. A folder was created on the Workflows page. |
| `folder.update` | **Folder changed**. A folder was renamed or moved. |
| `folder.delete` | **Folder deleted**. A folder was deleted. |
| `template.create` | **Template saved**. A workflow was saved as a template. |
| `template.update` | **Template changed**. A template was renamed or changed. |
| `template.replace_steps` | **Template steps updated**. A template's steps were replaced with a copy of a workflow. |
| `template.delete` | **Template deleted**. A template was deleted. |
| `template.use` | **Template used**. A workflow was created from a template. |
| `keel.selfheal.triggered` | **Keel offered a fix**. Keel opened a self-heal conversation for a failing workflow. |
| `keel.discard` | **Keel card discarded**. A person discarded one of Keel's proposal cards; nothing changed. |
| `keel.undo` | **Keel change undone**. A person undid a change Keel applied: the draft as it was before it, or the draft it created deleted. |
| `keel.test` | **Keel ran a test**. Keel ran a workflow's draft once in the test environment, with no click, to check its work. |
| `embed.workflow.create` | **Workflow installed (embed)**. The partner installed a workflow from a template for a customer account. |
| `embed.workflow.enable` | **Workflow turned on (embed)**. The partner turned a customer's workflow on. |
| `embed.workflow.disable` | **Workflow turned off (embed)**. The partner turned a customer's workflow off. |
| **Runs** |   |
| `run.start` High volume | **Run started**. A person or token started a run by hand (Run, Run again, or the API). |
| `run.rerun` | **Runs started again**. A person or token ran several runs again at once (Run again over a selection), each with the data it started with; the count, the new runs and the runs they retried are in `metadata`. |
| `run.cancel` | **Run cancelled**. A person or token cancelled a run in progress. |
| `run.decide` | **Approval answered**. A person or token approved or rejected a held run from the product. |
| `run.delete` | **Run deleted**. A run and its step data were deleted, one or several at once (the count in `metadata`). |
| `embed.run.cancel` | **Run cancelled (embed)**. The partner cancelled a customer’s run. |
| `embed.run.rerun` | **Run started again (embed)**. The partner ran a customer’s run again with the data it started with. |
| `run.completed` Streaming only | **Run completed**. A production run finished successfully. |
| `run.failed` Streaming only | **Run failed**. A production run failed. |
| `run.cancelled` Streaming only | **Run stopped**. A production run was cancelled before it finished. |
| `run.paused` Streaming only | **Run waiting**. A production run paused to wait for an approval, a delay or a webhook. |
| **Connections** |   |
| `connection.create` | **Connection added**. A connection to an outside service was added. |
| `connection.update` | **Connection changed**. A connection's name or settings were changed (field names only). |
| `connection.reconnect` | **Connection reconnected**. A connection's credentials were replaced, or an OAuth sign-in to the service completed (the first one included; `metadata.method`). |
| `connection.delete` | **Connection deleted**. A connection was deleted. |
| `connection.decrypt` High volume | **Credential used**. A stored credential was decrypted: by a run, a test, a dropdown or a connection check. |
| `connection.expired` Streaming only | **Connection expired**. A connection stopped working and needs reconnecting. |
| `embed.connection.create` | **Connection added (embed)**. The partner stored a connection for a customer account. |
| `embed.connection.update` | **Connection replaced (embed)**. The partner replaced a customer's connection. |
| `embed.connection.delete` | **Connection removed (embed)**. The partner removed a customer's connection. |
| **Members** |   |
| `installation.admin.created` | **First admin created**. The installation's first account was created; it owns the starter organization and is the platform admin. |
| `member.invite` | **Person invited**. An admin invited someone to the organization. |
| `member.invite.revoke` | **Invitation withdrawn**. An admin withdrew an invitation before it was accepted. |
| `member.invite.accept` | **Invitation accepted**. Someone accepted an invitation and joined the organization. |
| `member.role.update` | **Role changed**. A member's organization role was changed. |
| `member.remove` | **Member removed**. An admin removed someone from the organization. |
| `member.leave` | **Member left**. Someone left the organization. |
| `scim.user.create` | **Person provisioned (SCIM)**. The identity provider created an account through SCIM. |
| `scim.user.update` | **Person updated (SCIM)**. The identity provider changed or deactivated an account through SCIM. |
| `scim.user.delete` | **Person deprovisioned (SCIM)**. The identity provider removed an account through SCIM. |
| `embed.user.create` | **Person added (embed)**. The partner created a person for a customer account. |
| `embed.user.update` | **Person changed (embed)**. The partner changed a customer account's person. |
| `embed.user.delete` | **Person removed (embed)**. The partner removed a person. |
| `embed.member.add` | **Member added (embed)**. The partner gave a person access to a customer workspace. |
| `embed.member.role` | **Member role changed (embed)**. The partner changed a person's role in a customer workspace. |
| `embed.member.remove` | **Member removed (embed)**. The partner removed a person from a customer workspace. |
| **Workspaces** |   |
| `workspace.create` | **Workspace created**. A workspace was created. |
| `workspace.rename` | **Workspace renamed**. A workspace was renamed. |
| `workspace.delete` | **Workspace deleted**. A workspace and everything in it was deleted. |
| `workspace.joined` | **Joined workspace**. An organization owner or admin joined one of its workspaces as its admin. |
| `workspace.leave` | **Left workspace**. Someone left a workspace. |
| `workspace.member.set` | **Workspace access changed**. Someone was added to a workspace or their workspace role changed. |
| `workspace.member.remove` | **Workspace access removed**. Someone was removed from a workspace. |
| `embed.workspace.create` | **Workspace created (embed)**. The partner created a workspace for a customer account. |
| `embed.workspace.update` | **Workspace changed (embed)**. The partner changed a customer workspace. |
| `embed.workspace.delete` | **Workspace deleted (embed)**. The partner deleted a customer workspace. |
| **API tokens** |   |
| `token.create` | **API token created**. A workspace admin, an embedded editor or the partner’s server created an API token (its scopes in `metadata`, never the token). |
| `token.revoke` | **API token revoked**. A workspace admin, an embedded editor or the partner’s server revoked an API token. |
| `oauth.consent.granted` | **App connected**. A person connected an outside application (an AI agent) to a workspace. |
| `oauth.consent.superseded` | **App connection replaced**. Connecting an application again retired its previous grant. |
| `oauth.consent.revoked` | **App disconnected**. A person disconnected an outside application. |
| `oauth.grant.workspaces_changed` | **App workspaces changed**. A person changed which workspaces a connected application reaches. |
| `embed.oauth.signed_in` | **AI app sign-in completed**. The partner's server signed one of the account's people in to connect an AI app (the app's name and where its code goes). |
| `embed.oauth.denied` | **AI app sign-in refused**. The partner's server refused an AI app's sign-in to the account (and said why). |
| **Security** |   |
| `installation.setup.used` | **Installation set up**. The installation's first admin was created with the setup code the server printed (`metadata.method`: link or password). |
| `account.mfa.enable` | **Two-factor turned on**. A person turned on two-factor authentication for their account. |
| `account.mfa.disable` | **Two-factor turned off**. A person turned off two-factor authentication for their account. |
| `account.mfa.recovery.regen` | **Recovery codes replaced**. A person generated a new set of two-factor recovery codes; the old ones stop working. |
| `account.password.set` | **Password set**. A person set their password as their account was created (self-hosted, no mail). |
| `account.password.change` | **Password changed**. A person changed their own password; their other sessions ended. |
| `account.password.reset` | **Password reset**. A person set a new password with a reset link; every session they had ended. |
| `account.password.cleared` | **Password removed**. A person signed in with an emailed link once mail was set up, so the password they had was removed. |
| `account.password.reset_link` | **Reset link made**. An owner or admin (or the operator on the command line) made a one-time password reset link for a person. |
| `sso.oidc.update` | **OpenID Connect configured**. An admin set up or changed the organization's OpenID Connect single sign-on (field names only). |
| `sso.oidc.delete` | **OpenID Connect removed**. An admin removed the organization's OpenID Connect single sign-on. |
| `sso.saml.update` | **SAML configured**. An admin set up or changed the organization's SAML single sign-on (field names only). |
| `sso.saml.delete` | **SAML removed**. An admin removed the organization's SAML single sign-on. |
| `scim.token.create` | **SCIM token created**. An admin created the organization's SCIM provisioning token (shown once). |
| `scim.token.revoke` | **SCIM token revoked**. An admin revoked the organization's SCIM provisioning token. |
| `oauth.token.reuse_detected` | **Connected app token reuse**. A spent refresh token was presented again, so the whole grant was revoked. |
| `audit.export` | **Audit log exported**. An admin downloaded the audit log as CSV (filters and row count in `metadata`). |
| **Organization** |   |
| `org.create` | **Organization created**. A person created a new organization. |
| `org.rename` | **Organization renamed**. An admin renamed the organization. |
| `org.transfer` | **Ownership transferred**. The organization was handed to another member. |
| `org.delete` | **Organization deleted**. The organization and everything in it was deleted. |
| `ai.provider.update` | **AI provider configured**. An admin added or changed an AI provider for Keel (field names only; never the key). |
| `ai.provider.default` | **Default AI provider changed**. An admin made an AI provider the organization's default. |
| `ai.provider.delete` | **AI provider removed**. An admin removed an AI provider. |
| `embed.account.create` | **Account created (embed)**. The partner created this customer account. |
| `embed.account.update` | **Account changed (embed)**. The partner (or a platform admin) changed this customer account. |
| `embed.account.suspend` | **Account suspended**. The customer account was suspended. |
| `embed.account.resume` | **Account resumed**. The customer account was resumed. |
| `embed.account.delete` | **Account deleted (embed)**. The customer account was deleted (restorable until purged). |
| `embed.account.restore` | **Account restored**. A deleted customer account was restored. |
| `embed.account.purge` | **Account purged**. A deleted customer account's data was permanently removed. |
| `embed.account.websites.set` | **Websites changed (embed)**. The partner’s server changed the websites that can show this customer’s sessions. |
| **Event streaming** |   |
| `stream.destination.create` | **Destination added**. An admin added an event-streaming destination. |
| `stream.destination.update` | **Destination changed**. An admin changed an event-streaming destination (field names only). |
| `stream.destination.delete` | **Destination removed**. An admin removed an event-streaming destination. |
| `stream.destination.rotate` | **Signing secret replaced**. An admin replaced a webhook destination's signing secret. |
| `stream.destination.test` | **Test event sent**. An admin sent a test event to a destination. |
| `stream.destination.replay` | **Deliveries replayed**. An admin sent one or more past deliveries again. |
| **Platform** |   |
| `platform.enter` | **Staff visit started**. A platform admin opened the organization as a staff visit. |
| `platform.elevate` | **Staff visit unlocked**. A visiting platform admin gave their two-factor code to make changes. |
| `platform.write` | **Staff change**. A change a visiting platform admin made (method, route and status; never the body). |
| `platform.leave` | **Staff visit ended**. A staff visit ended (left, timed out, or ended for a reason in `metadata`). |
| `platform.org.suspend` | **Organization suspended**. A platform admin suspended the organization: nothing runs in it, and no webhook or form is taken, until it is resumed. |
| `platform.org.resume` | **Organization resumed**. A platform admin resumed the suspended organization. |
| `platform.org.pieces.set` | **Apps allowed changed**. A platform admin changed which apps the organization may use. |
| `platform.assistant.limit` | **Assistant allowance changed**. A platform admin changed the organization's monthly assistant allowance. |
| `platform.embed.preview.start` | **Embed preview opened**. A platform admin opened the embed as one of the account's people. |
| `embed.preview.write` | **Embed preview change**. A change made while a platform admin previewed the embed. |
| `embed.api.call` High volume | **Partner API call**. One call to the embed API by the partner's server (route and status). |

## What a streamed event looks like

Every destination kind gets the same JSON: `id`, `type`, `timestamp` and `data`. An audit event also names who did it (`actor`) and to what (`target`).

```json
{
  "id": "e6fd5c2e-8fbb-4d36-a6cc-67e8a6cd9898",
  "type": "run.failed",
  "timestamp": "2026-10-03T10:02:16Z",
  "data": {
    "test": false,
    "label": "Run failed",
    "organization": { "id": "7f3e2a10-5c4b-4e8e-9a51-0d2c6b1f4e77", "name": "Acme" },
    "workspace": { "id": "50bb03eb-627c-4ed9-89ad-00cdf2051248", "name": "Sales" },
    "workflow": { "id": "581cb066-9cba-40b7-9b7e-8562ff362da3", "name": "New lead to CRM" },
    "run": {
      "id": "06e0ed27-4b87-4005-bd06-0856ba3faf39",
      "status": "failed",
      "trigger": "webhook",
      "environment": "production",
      "startedAt": "2026-10-03T10:01:31Z",
      "firstStartedAt": "2026-10-03T10:01:34Z",
      "finishedAt": "2026-10-03T10:02:16Z",
      "durationMs": 42000,
      "error": {
        "message": "The service answered 500 Internal Server Error.",
        "step": { "name": "notify_sales", "displayName": "Tell sales" }
      }
    },
    "link": "https://app.gorunner.io/workflows/581cb066-9cba-40b7-9b7e-8562ff362da3/runs/06e0ed27-4b87-4005-bd06-0856ba3faf39"
  }
}
```

**Every field in `data`**

- `test`: `true` only on a sample from **Send test**.
- `label`: The event in plain words, as the dashboard shows it.
- `organization`: `{ id, name }`.
- `workspace`: `{ id, name }`, or `null` for something done to the organization itself, such as a sign-in or a member’s role.
- `actor`: Audit events: `type`, `id`, `email`, `label` and, when known, `ip`. The type is `user`, `machine` (an API token), `keel`, `system`, `platform_admin` (a staff or admin visit) or `anonymous` (a refused sign-in).
- `target`: Audit events: `{ type, id, name }`.
- `workflow`: `{ id, name }`, when it is about a workflow.
- `run`: Run events, as above. `firstStartedAt` is absent on runs from v2026.10.5 or earlier. `run.paused` adds `wait: { step, waitingOn, pausedAt, resumeAt }`, where `waitingOn` is `person` or `time`.
- `connection`: `connection.expired`: `{ id, name, piece: { id, name }, expiredAt }`.
- `details`: Some audit events: the facts the audit row keeps, such as ids, names, roles and the names of changed fields. Never a secret or workflow data.
- `link`: Where it is in the dashboard.

A run’s error is cut at 500 bytes, with anything that looks like a key removed. Never sent: step input or output, what a trigger received, keys, variable values, Code, Keel conversations and approval links.

## Check a webhook is genuine

Each request has `webhook-id`, `webhook-timestamp` and `webhook-signature`, in the [Standard Webhooks](https://www.standardwebhooks.com) format. Check the raw body before you parse it:

**Node.js**

```js
import express from "express"
import { Webhook } from "standardwebhooks" // npm install standardwebhooks

const app = express()
const wh = new Webhook(process.env.GORUNNER_SIGNING_SECRET) // "whsec_…"

app.post("/gorunner-events", express.raw({ type: "application/json" }), (req, res) => {
  let event
  try {
    // Throws when the signature is wrong or the time is over 5 minutes off.
    event = wh.verify(req.body, req.headers)
  } catch {
    return res.status(400).end()
  }
  if (event.type === "run.failed") console.log(event.data.workflow.name, "failed")
  res.status(204).end()
})

app.listen(3000)
```

**Python**

```python
import os
from flask import Flask, abort, request
from standardwebhooks.webhooks import Webhook  # pip install standardwebhooks

app = Flask(__name__)
wh = Webhook(os.environ["GORUNNER_SIGNING_SECRET"])  # "whsec_…"

@app.post("/gorunner-events")
def gorunner_events():
    try:
        # Raises when the signature is wrong or the time is over 5 minutes off.
        event = wh.verify(request.get_data(), dict(request.headers))
    except Exception:
        abort(400)
    if event["type"] == "run.failed":
        print(event["data"]["workflow"]["name"], "failed")
    return "", 204
```

**Go**

```
package events

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// Verify checks a GoRunner event's signature. body is the request body exactly as it arrived.
func Verify(secret string, h http.Header, body []byte) error {
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return err
	}
	id, ts := h.Get("webhook-id"), h.Get("webhook-timestamp")
	sent, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return errors.New("no timestamp")
	}
	if d := time.Since(time.Unix(sent, 0)); d > 5*time.Minute || d < -5*time.Minute {
		return errors.New("timestamp too far from now")
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(id + "." + ts + "."))
	mac.Write(body)
	want := mac.Sum(nil)
	// While a secret is rotated, the header carries two signatures: either may match.
	for _, s := range strings.Fields(h.Get("webhook-signature")) {
		version, sig, ok := strings.Cut(s, ",")
		if !ok || version != "v1" {
			continue
		}
		if got, err := base64.StdEncoding.DecodeString(sig); err == nil && hmac.Equal(got, want) {
			return nil
		}
	}
	return errors.New("signature does not match")
}
```

The signature is `v1,` and a base64 HMAC-SHA256 of `id.timestamp.body`, keyed with the base64-decoded bytes after `whsec_`. **Rotate secret…** gives a new one; the old one keeps signing for 24 hours, so a request then carries two signatures.

## Streaming limits

- **Destinations**: 25 for one organization.
- **Events per destination**: 200 events or groups.
- **Tests before saving**: 20 a minute for one organization.
- **Name**: 200 characters.
- **Datadog tags**: 50.
- **OpenTelemetry headers**: 20, each one line of up to 4,096 characters.

Related: [Audit log](https://docs.gorunner.io/build/audit-log) · [Event streaming](https://docs.gorunner.io/build/event-streaming) · [The platform audit log](https://docs.gorunner.io/self-host/platform-audit-log)

Source: https://docs.gorunner.io/build/events
