Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/components/Rootly.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";

<CardGrid>
<LinkCard title="On Incident" href="#on-incident" description="Listen to incident events" />
<LinkCard title="On Incident Timeline Event" href="#on-incident-timeline-event" description="Listen to incident timeline events" />
</CardGrid>

## Actions
Expand Down Expand Up @@ -71,6 +72,87 @@ This trigger automatically sets up a Rootly webhook endpoint when configured. Th
}
```

<a id="on-incident-timeline-event"></a>

## On Incident Timeline Event

The On Incident Timeline Event trigger starts a workflow execution when Rootly incident timeline events are created or updated.
Only events with kind "event" are emitted.

### Use Cases

- **Note automation**: Run workflows when investigation notes are added
- **Timeline sync**: Sync incident timeline events to Slack or external systems
- **Annotation tracking**: Track updates to incident annotations
- **Audit logging**: Capture timeline events for compliance or reporting

### Configuration

- **Incident Status**: Optional filter by incident status (open, resolved, etc.)
- **Severity**: Optional filter by incident severity
- **Service**: Optional filter by service name
- **Team**: Optional filter by team name
- **Event Source**: Optional filter by event source (web, api, system)
- **Visibility**: Optional filter by event visibility (internal or external)

### Event Data

Each incident event includes:
- **id**: Event ID
- **event**: Event content
- **event_raw**: Raw event content
- **event_id**: Webhook event ID
- **event_type**: Event type (incident_event.created or incident_event.updated)
- **kind**: Event kind
- **source**: Event source
- **visibility**: Event visibility
- **occurred_at**: When the event occurred
- **created_at**: When the event was created
- **updated_at**: When the event was last updated
- **issued_at**: When the webhook was issued
- **incident_id**: Incident ID
- **incident**: Incident information

### Webhook Setup

This trigger automatically sets up a Rootly webhook endpoint when configured. The endpoint is managed by SuperPlane and will be cleaned up when the trigger is removed.

### Example Data

```json
{
"data": {
"created_at": "2026-02-22T09:46:23.868-08:00",
"event": "Investigation started, will update accordingly",
"event_id": "b3065ca8-69a6-4781-b6b4-94d6f0317ccf",
"event_raw": "Investigation started, will update accordingly",
"event_type": "incident_event.created",
"id": "56f7b488-e3c5-4091-9bb4-cf132007f98c",
"incident": {
"id": "64c39fde-1626-4f78-874e-9db91c0639d3",
"services": [
"UI - User Profile Block"
],
"severity": "sev2",
"status": "mitigated",
"teams": [
"Customer Relations"
],
"title": "new remake from main"
},
"incident_id": "64c39fde-1626-4f78-874e-9db91c0639d3",
"issued_at": "2026-02-22T09:46:24.018-08:00",
"kind": "event",
"occurred_at": "2026-02-22T09:46:23.868-08:00",
"source": "web",
"updated_at": "2026-02-22T09:46:23.868-08:00",
"visibility": "internal"
},
"timestamp": "2026-02-22T17:46:40.603539728Z",
"type": "rootly.onIncidentTimelineEvent"
}
```

<a id="create-event"></a>

## Create Event
Expand Down
120 changes: 109 additions & 11 deletions pkg/integrations/rootly/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,19 +259,43 @@ type IncidentEventResponse struct {
Data IncidentEventData `json:"data"`
}

// severityString extracts the severity slug from the API response.
// Rootly returns severity as a string (slug) or an object with slug/name fields.
// severityString extracts a severity slug/name from Rootly responses.
// Rootly may return severity as a string, a flat object, or a JSON:API nested object.
func severityString(v any) string {
switch s := v.(type) {
case string:
// Severity already provided as a slug/name string.
return s
case map[string]any:
if slug, ok := s["slug"].(string); ok {
return slug
// Severity provided as a flat object with slug/name fields.
if value := severityFromMap(s); value != "" {
return value
}
if name, ok := s["name"].(string); ok {
return name

// Severity provided as a nested JSON:API object: {data:{attributes:{slug/name}}}.
data, ok := s["data"].(map[string]any)
if !ok {
return ""
}

attrs, ok := data["attributes"].(map[string]any)
if !ok {
return ""
}

return severityFromMap(attrs)
default:
return ""
}
}

func severityFromMap(values map[string]any) string {
if slug, ok := values["slug"].(string); ok && slug != "" {
return slug
}

if name, ok := values["name"].(string); ok && name != "" {
return name
}

return ""
Expand Down Expand Up @@ -709,9 +733,11 @@ func (c *Client) UpdateIncident(id string, attrs UpdateIncidentAttributes) (*Inc

// WebhookEndpoint represents a Rootly webhook endpoint
type WebhookEndpoint struct {
ID string `json:"id"`
URL string `json:"url"`
Secret string `json:"secret"`
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Secret string `json:"secret"`
Events []string `json:"event_types"`
}

type WebhookEndpointData struct {
Expand All @@ -721,6 +747,7 @@ type WebhookEndpointData struct {
}

type WebhookEndpointAttributes struct {
Name string `json:"name"`
URL string `json:"url"`
Secret string `json:"secret"`
EventTypes []string `json:"event_types"`
Expand All @@ -732,6 +759,10 @@ type WebhookEndpointResponse struct {
Data WebhookEndpointData `json:"data"`
}

type WebhookEndpointsResponse struct {
Data []WebhookEndpointData `json:"data"`
}

type CreateWebhookEndpointRequest struct {
Data CreateWebhookEndpointData `json:"data"`
}
Expand All @@ -749,12 +780,12 @@ type CreateWebhookEndpointAttributes struct {
SigningEnabled bool `json:"signing_enabled"`
}

func (c *Client) CreateWebhookEndpoint(url string, events []string) (*WebhookEndpoint, error) {
func (c *Client) CreateWebhookEndpoint(name, url string, events []string) (*WebhookEndpoint, error) {
request := CreateWebhookEndpointRequest{
Data: CreateWebhookEndpointData{
Type: "webhooks_endpoints",
Attributes: CreateWebhookEndpointAttributes{
Name: "SuperPlane",
Name: name,
URL: url,
EventTypes: events,
Enabled: true,
Expand Down Expand Up @@ -782,8 +813,75 @@ func (c *Client) CreateWebhookEndpoint(url string, events []string) (*WebhookEnd

return &WebhookEndpoint{
ID: response.Data.ID,
Name: response.Data.Attributes.Name,
URL: response.Data.Attributes.URL,
Secret: response.Data.Attributes.Secret,
Events: response.Data.Attributes.EventTypes,
}, nil
}

func (c *Client) ListWebhookEndpoints() ([]WebhookEndpoint, error) {
apiURL := fmt.Sprintf("%s/webhooks/endpoints", c.BaseURL)
responseBody, err := c.execRequest(http.MethodGet, apiURL, nil)
if err != nil {
return nil, err
}

var response WebhookEndpointsResponse
if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

endpoints := make([]WebhookEndpoint, 0, len(response.Data))
for _, data := range response.Data {
endpoints = append(endpoints, WebhookEndpoint{
ID: data.ID,
Name: data.Attributes.Name,
URL: data.Attributes.URL,
Secret: data.Attributes.Secret,
Events: data.Attributes.EventTypes,
})
}

return endpoints, nil
}

func (c *Client) UpdateWebhookEndpoint(id, name, url string, events []string, enabled bool) (*WebhookEndpoint, error) {
request := CreateWebhookEndpointRequest{
Data: CreateWebhookEndpointData{
Type: "webhooks_endpoints",
Attributes: CreateWebhookEndpointAttributes{
Name: name,
URL: url,
EventTypes: events,
Enabled: enabled,
SigningEnabled: true,
},
},
}

body, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("error marshaling request: %v", err)
}

apiURL := fmt.Sprintf("%s/webhooks/endpoints/%s", c.BaseURL, id)
responseBody, err := c.execRequest(http.MethodPut, apiURL, bytes.NewReader(body))
if err != nil {
return nil, err
}

var response WebhookEndpointResponse
if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

return &WebhookEndpoint{
ID: response.Data.ID,
Name: response.Data.Attributes.Name,
URL: response.Data.Attributes.URL,
Secret: response.Data.Attributes.Secret,
Events: response.Data.Attributes.EventTypes,
}, nil
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/integrations/rootly/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ var exampleDataOnIncidentBytes []byte
var exampleDataOnIncidentOnce sync.Once
var exampleDataOnIncident map[string]any

//go:embed example_data_on_incident_timeline_event.json
var exampleDataOnIncidentTimelineEventBytes []byte

var exampleDataOnIncidentTimelineEventOnce sync.Once
var exampleDataOnIncidentTimelineEvent map[string]any

func (c *CreateIncident) ExampleOutput() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleOutputCreateIncidentOnce, exampleOutputCreateIncidentBytes, &exampleOutputCreateIncident)
}
Expand All @@ -56,3 +62,7 @@ var exampleOutputGetIncident map[string]any
func (c *GetIncident) ExampleOutput() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleOutputGetIncidentOnce, exampleOutputGetIncidentBytes, &exampleOutputGetIncident)
}

func (t *OnIncidentTimelineEvent) ExampleData() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleDataOnIncidentTimelineEventOnce, exampleDataOnIncidentTimelineEventBytes, &exampleDataOnIncidentTimelineEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"data": {
"created_at": "2026-02-22T09:46:23.868-08:00",
"event": "Investigation started, will update accordingly",
"event_id": "b3065ca8-69a6-4781-b6b4-94d6f0317ccf",
"event_raw": "Investigation started, will update accordingly",
"event_type": "incident_event.created",
"id": "56f7b488-e3c5-4091-9bb4-cf132007f98c",
"incident": {
"id": "64c39fde-1626-4f78-874e-9db91c0639d3",
"services": [
"UI - User Profile Block"
],
"severity": "sev2",
"status": "mitigated",
"teams": [
"Customer Relations"
],
"title": "new remake from main"
},
"incident_id": "64c39fde-1626-4f78-874e-9db91c0639d3",
"issued_at": "2026-02-22T09:46:24.018-08:00",
"kind": "event",
"occurred_at": "2026-02-22T09:46:23.868-08:00",
"source": "web",
"updated_at": "2026-02-22T09:46:23.868-08:00",
"visibility": "internal"
},
"timestamp": "2026-02-22T17:46:40.603539728Z",
"type": "rootly.onIncidentTimelineEvent"
}
Loading