> For the complete documentation index, see [llms.txt](/llms.txt).
> A full single-fetch corpus is available at [llms-full.txt](/llms-full.txt).
---
title: Sentry
description: Start AGNT5 workflows automatically from Sentry issues, errors, and comments.
last_verified: 2026-07-24
---



Connect Sentry to AGNT5 and your workflows will start automatically whenever an issue, error, or comment event fires. Every delivery is signature-verified before any workflow runs.

---

## Setup

### 1. Create the integration in Studio

1. Go to **Settings → Integrations** and click **Add Integration**.
2. Pick **Sentry**.
3. Fill in a name for the integration.
4. Choose the environment that should receive triggers.

<DocsImage
  src="/docs/integrations/sentry-integration-light.png"
  darkSrc="/docs/integrations/sentry-integration-dark.png"
  alt="Studio New Sentry Webhook panel showing Name, Environment, Webhook URL, and Signing Secret fields."
  caption="Fill in the name, select the environment, copy the webhook URL, and paste the signing secret from Sentry."
  width={1746}
  height={1060}
/>

### 2. Create an integration in Sentry

1. Go to [**Settings → Integrations → Custom Integrations**](https://agnt5.sentry.io/settings/developer-settings/) and click **Create New Integration**.
2. Fill in the required details and paste the webhook URL from AGNT5 Studio.
3. Under **Permissions**, configure access for each resource. For example:

   <Callout type="info">
   | Permission | Value |
   |---|---|
   | Project | Read |
   | Team | Read |
   | Release | No Access |
   | Distribution | No Access |
   | Issue & Event | Read |
   | Organization | Read |
   | Member | Read |
   | Alerts | No Access |
   </Callout>

4. Under **Webhooks**, check the event types you want to trigger workflows. For example:

   <Callout type="info">
   issue, error, comment, seer, preprod_artifact
   </Callout>
5. Click **Save Changes**.
6. Copy the **Client Secret** and paste it into the **Signing secret** field in Studio.

_[screenshot: Sentry Custom Integrations page with the Create New Integration button and the Client Secret field highlighted]_

---

## Events

Each Sentry event has a name in the format `sentry.<resource>.<action>`. Pass this name to `event()` in your workflow trigger.

| Resource | Action | Full event name |
|---|---|---|
| `issue` | `created` | `sentry.issue.created` |
| `issue` | `resolved` | `sentry.issue.resolved` |
| `issue` | `assigned` | `sentry.issue.assigned` |
| `issue` | `ignored` | `sentry.issue.ignored` |
| `error` | `created` | `sentry.error.created` |
| `comment` | `created` | `sentry.comment.created` |

---

## Workflow examples

### Issue events



**Python:**

Workflows receive the webhook envelope as keyword arguments (`**envelope`). The `body` field is the raw verified request body. It may arrive as a JSON string or an already-parsed dict, so parse defensively.

```python
interface WebhookEnvelope {
  event_type: string;
  body: string;
}

function parseBody(envelope: WebhookEnvelope): Record<string, any> {
  try {
    return JSON.parse(envelope.body ?? '{}');
  } catch {
    return {};
  }
}

export const triageSentryIssue = workflow(
  'triage_sentry_issue',
  async (ctx: Context, envelope: WebhookEnvelope) => {
    const body = parseBody(envelope);
    const issue = body.data?.issue ?? {};

    ctx.logger.info(`Sentry issue.created: ${issue.title ?? 'unknown'} (level=${issue.level ?? 'unknown'})`);

    // add your triage logic here, e.g. open a ticket, page on-call
    return {
      issueId: issue.id,
      title: issue.title,
      level: issue.level,
      eventType: envelope.event_type,
    };
  },
  { triggers: [event('sentry.issue.created')] },
);
```





**Go:**

Workflows receive the webhook envelope as an untyped `map[string]any` — there's no typed envelope struct. `body` is the raw verified request body, always a string, so parse it defensively.

```go
import (
    "github.com/agnt5dev/sdk-go/agnt5"
    "encoding/json"
)

func parseBody(envelope map[string]any) map[string]any {
    body, _ := envelope["body"].(string)
    var parsed map[string]any
    if err := json.Unmarshal([]byte(body), &parsed); err != nil {
        return map[string]any{}
    }
    return parsed
}

err := agnt5.RegisterWorkflow(worker, "triage_sentry_issue",
    func(ctx *agnt5.Context, envelope map[string]any) (TriageResult, error) {
        body := parseBody(envelope)
        data, _ := body["data"].(map[string]any)
        issue, _ := data["issue"].(map[string]any)

        title, _ := issue["title"].(string)
        level, _ := issue["level"].(string)
        ctx.Logger().Info("Sentry issue.created", "title", title, "level", level)

        // add your triage logic here, e.g. open a ticket, page on-call
        eventType, _ := envelope["event_type"].(string)
        return TriageResult{IssueID: issue["id"], Title: title, Level: level, EventType: eventType}, nil
    },
    agnt5.WithTriggers(agnt5.EventTrigger("sentry.issue.created")),
)
```



### Comment events



**Python:**

Comment payloads nest the comment text several levels deep. Extract defensively:

```python
@workflow(
    name="log_sentry_comment",
    triggers=[event("sentry.comment.created")],
)
async def log_sentry_comment(ctx: WorkflowContext, **envelope) -> dict:
    body = _parse_body(envelope)
    data = body.get("data", {})
    comment = data.get("comment") or body.get("comment") or {}
    issue = data.get("issue") or body.get("issue") or {}
    actor = body.get("actor") or {}
    user = comment.get("user") or comment.get("author") or {}

    author = (
        comment.get("author_name")
        or user.get("name")
        or actor.get("name")
        or "unknown"
    )
    text = (
        comment.get("text")
        or comment.get("message")
        or data.get("text")
        or ""
    )

    ctx.logger.info(
        "Sentry comment from %s on %s: %s"
        % (author, issue.get("short_id", "unknown"), text[:200])
    )
    return {"author": author, "issue": issue.get("short_id"), "comment": text}
```





**TypeScript:**

Comment payloads nest the comment text several levels deep. Extract defensively:

```typescript
export const logSentryComment = workflow(
  'log_sentry_comment',
  async (ctx: Context, envelope: WebhookEnvelope) => {
    const body = parseBody(envelope);
    const data = body.data ?? {};
    const comment = data.comment ?? body.comment ?? {};
    const issue = data.issue ?? body.issue ?? {};
    const actor = body.actor ?? {};
    const user = comment.user ?? comment.author ?? {};

    const author = comment.author_name ?? user.name ?? actor.name ?? 'unknown';
    const text = comment.text ?? comment.message ?? data.text ?? '';

    ctx.logger.info(`Sentry comment from ${author} on ${issue.short_id ?? 'unknown'}: ${text.slice(0, 200)}`);
    return { author, issue: issue.short_id, comment: text };
  },
  { triggers: [event('sentry.comment.created')] },
);
```





**Go:**

Comment payloads nest the comment text several levels deep. Extract defensively with type assertions. `firstMap`, `firstString`, and `truncate` here are small local helpers you write once (first non-nil `map[string]any`/`string` argument, and a length-bounded substring, respectively) — the standard library has no built-in for either:

```go
err := agnt5.RegisterWorkflow(worker, "log_sentry_comment",
    func(ctx *agnt5.Context, envelope map[string]any) (CommentLog, error) {
        body := parseBody(envelope)
        data, _ := body["data"].(map[string]any)
        comment, _ := firstMap(data["comment"], body["comment"])
        issue, _ := firstMap(data["issue"], body["issue"])
        actor, _ := body["actor"].(map[string]any)
        user, _ := firstMap(comment["user"], comment["author"])

        author := firstString(comment["author_name"], user["name"], actor["name"], "unknown")
        text := firstString(comment["text"], comment["message"], data["text"], "")
        shortID, _ := issue["short_id"].(string)

        ctx.Logger().Info("Sentry comment", "author", author, "issue", shortID, "text", truncate(text, 200))
        return CommentLog{Author: author, Issue: shortID, Comment: text}, nil
    },
    agnt5.WithTriggers(agnt5.EventTrigger("sentry.comment.created")),
)
```



### Viewing triggered runs in Studio

After a Sentry event fires, the run appears in **Studio → Runs** within seconds. Open the run to see the envelope inputs and workflow output in the trace.

_[screenshot: Studio Runs list showing a completed triage_sentry_issue run, with the run detail open showing the sentry.issue.created event_type and the workflow output]_

---

## Envelope structure

Every Sentry-triggered run receives this envelope as `**envelope`:

```json
{
  "_webhook": true,
  "source": "sentry",
  "integration_id": "int_abc123",
  "event_type": "sentry.issue.created",
  "idempotency_key": "req_9f3c…",
  "timestamp": 1733337600,
  "headers": { "sentry-hook-resource": "issue", "request-id": "req_9f3c…" },
  "body": "{\"action\":\"created\",\"data\":{\"issue\":{…}}}"
}
```

`body` is the raw signature-verified request body. Parse it with `json.loads` before use.

---

## Signature verification

- Every delivery is verified using `sentry-hook-signature` before any workflow runs.
- Requests with an invalid or missing signature are rejected with `401`.

---

## Delivery semantics

- Sentry retries failed deliveries automatically.
- AGNT5 deduplicates using the `Request-ID` header.
- A retry replays the original run instead of starting a new one.

---

## Related

- [Event sources overview](/docs/integrations/event-sources/overview.md)
- [Webhooks](/docs/build/webhooks.md): trigger mechanism, signature verification, and delivery semantics
- [Workflows](/docs/build/workflows.md): how to write and run workflows
