diff --git a/docs/components/Rootly.mdx b/docs/components/Rootly.mdx
index e107de59a2..d26991baea 100644
--- a/docs/components/Rootly.mdx
+++ b/docs/components/Rootly.mdx
@@ -17,6 +17,7 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
+
@@ -160,6 +161,59 @@ Returns the created incident object including:
}
```
+
+
+## Get Incident
+
+The Get Incident component retrieves a single incident from Rootly by its ID.
+
+### Use Cases
+
+- **Incident enrichment**: Fetch current incident details to use in downstream workflow steps
+- **Status checks**: Check the current status of an incident before taking action
+- **Data retrieval**: Pull incident information for reporting or notifications
+
+### Configuration
+
+- **Incident ID**: The UUID of the incident to retrieve (required, supports expressions)
+
+### Output
+
+Returns the incident object including:
+- **id**: Incident UUID
+- **sequential_id**: Sequential incident number
+- **title**: Incident title
+- **slug**: URL-friendly slug
+- **summary**: Incident summary
+- **status**: Current incident status
+- **severity**: Incident severity level
+- **started_at**: When the incident started
+- **mitigated_at**: When the incident was mitigated
+- **resolved_at**: When the incident was resolved
+- **updated_at**: Last update timestamp
+- **url**: Link to the incident in Rootly
+
+### Example Output
+
+```json
+{
+ "data": {
+ "id": "abc123-def456",
+ "sequential_id": 42,
+ "severity": "sev1",
+ "slug": "database-connection-issues",
+ "started_at": "2026-01-19T12:00:00Z",
+ "status": "started",
+ "summary": "Users are experiencing slow database queries and connection timeouts.",
+ "title": "Database connection issues",
+ "updated_at": "2026-01-19T12:00:00Z",
+ "url": "https://app.rootly.com/incidents/abc123-def456"
+ },
+ "timestamp": "2026-01-19T12:00:00Z",
+ "type": "rootly.incident"
+}
+```
+
## Update Incident
diff --git a/pkg/integrations/rootly/example.go b/pkg/integrations/rootly/example.go
index 89e88c19bf..dcfae178cd 100644
--- a/pkg/integrations/rootly/example.go
+++ b/pkg/integrations/rootly/example.go
@@ -25,6 +25,12 @@ var exampleOutputUpdateIncidentBytes []byte
var exampleOutputUpdateIncidentOnce sync.Once
var exampleOutputUpdateIncident map[string]any
+//go:embed example_output_get_incident.json
+var exampleOutputGetIncidentBytes []byte
+
+var exampleOutputGetIncidentOnce sync.Once
+var exampleOutputGetIncident map[string]any
+
//go:embed example_data_on_incident.json
var exampleDataOnIncidentBytes []byte
@@ -43,6 +49,10 @@ func (c *UpdateIncident) ExampleOutput() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleOutputUpdateIncidentOnce, exampleOutputUpdateIncidentBytes, &exampleOutputUpdateIncident)
}
+func (c *GetIncident) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputGetIncidentOnce, exampleOutputGetIncidentBytes, &exampleOutputGetIncident)
+}
+
func (t *OnIncident) ExampleData() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleDataOnIncidentOnce, exampleDataOnIncidentBytes, &exampleDataOnIncident)
}
diff --git a/pkg/integrations/rootly/example_output_get_incident.json b/pkg/integrations/rootly/example_output_get_incident.json
new file mode 100644
index 0000000000..00543aedf2
--- /dev/null
+++ b/pkg/integrations/rootly/example_output_get_incident.json
@@ -0,0 +1,16 @@
+{
+ "type": "rootly.incident",
+ "data": {
+ "id": "abc123-def456",
+ "sequential_id": 42,
+ "title": "Database connection issues",
+ "slug": "database-connection-issues",
+ "summary": "Users are experiencing slow database queries and connection timeouts.",
+ "status": "started",
+ "severity": "sev1",
+ "started_at": "2026-01-19T12:00:00Z",
+ "updated_at": "2026-01-19T12:00:00Z",
+ "url": "https://app.rootly.com/incidents/abc123-def456"
+ },
+ "timestamp": "2026-01-19T12:00:00Z"
+}
diff --git a/pkg/integrations/rootly/get_incident.go b/pkg/integrations/rootly/get_incident.go
new file mode 100644
index 0000000000..f26acc2be5
--- /dev/null
+++ b/pkg/integrations/rootly/get_incident.go
@@ -0,0 +1,147 @@
+package rootly
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+)
+
+type GetIncident struct{}
+
+type GetIncidentSpec struct {
+ IncidentID string `json:"incidentId"`
+}
+
+func (c *GetIncident) Name() string {
+ return "rootly.getIncident"
+}
+
+func (c *GetIncident) Label() string {
+ return "Get Incident"
+}
+
+func (c *GetIncident) Description() string {
+ return "Retrieve a single incident from Rootly by ID"
+}
+
+func (c *GetIncident) Documentation() string {
+ return `The Get Incident component retrieves a single incident from Rootly by its ID.
+
+## Use Cases
+
+- **Incident enrichment**: Fetch current incident details to use in downstream workflow steps
+- **Status checks**: Check the current status of an incident before taking action
+- **Data retrieval**: Pull incident information for reporting or notifications
+
+## Configuration
+
+- **Incident ID**: The UUID of the incident to retrieve (required, supports expressions)
+
+## Output
+
+Returns the incident object including:
+- **id**: Incident UUID
+- **sequential_id**: Sequential incident number
+- **title**: Incident title
+- **slug**: URL-friendly slug
+- **summary**: Incident summary
+- **status**: Current incident status
+- **severity**: Incident severity level
+- **started_at**: When the incident started
+- **mitigated_at**: When the incident was mitigated
+- **resolved_at**: When the incident was resolved
+- **updated_at**: Last update timestamp
+- **url**: Link to the incident in Rootly`
+}
+
+func (c *GetIncident) Icon() string {
+ return "search"
+}
+
+func (c *GetIncident) Color() string {
+ return "gray"
+}
+
+func (c *GetIncident) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *GetIncident) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "incidentId",
+ Label: "Incident ID",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Placeholder: "e.g., abc123-def456",
+ Description: "The UUID of the incident to retrieve",
+ },
+ }
+}
+
+func (c *GetIncident) Setup(ctx core.SetupContext) error {
+ spec := GetIncidentSpec{}
+ err := mapstructure.Decode(ctx.Configuration, &spec)
+ if err != nil {
+ return fmt.Errorf("error decoding configuration: %v", err)
+ }
+
+ if spec.IncidentID == "" {
+ return errors.New("incidentId is required")
+ }
+
+ return nil
+}
+
+func (c *GetIncident) Execute(ctx core.ExecutionContext) error {
+ spec := GetIncidentSpec{}
+ err := mapstructure.Decode(ctx.Configuration, &spec)
+ if err != nil {
+ return fmt.Errorf("error decoding configuration: %v", err)
+ }
+
+ client, err := NewClient(ctx.HTTP, ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("error creating client: %v", err)
+ }
+
+ incident, err := client.GetIncident(spec.IncidentID)
+ if err != nil {
+ return fmt.Errorf("failed to get incident: %v", err)
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "rootly.incident",
+ []any{incident},
+ )
+}
+
+func (c *GetIncident) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *GetIncident) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *GetIncident) Actions() []core.Action {
+ return []core.Action{}
+}
+
+func (c *GetIncident) HandleAction(ctx core.ActionContext) error {
+ return nil
+}
+
+func (c *GetIncident) HandleWebhook(ctx core.WebhookRequestContext) (int, error) {
+ return http.StatusOK, nil
+}
+
+func (c *GetIncident) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
diff --git a/pkg/integrations/rootly/get_incident_test.go b/pkg/integrations/rootly/get_incident_test.go
new file mode 100644
index 0000000000..1291db042a
--- /dev/null
+++ b/pkg/integrations/rootly/get_incident_test.go
@@ -0,0 +1,151 @@
+package rootly
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__GetIncident__Setup(t *testing.T) {
+ component := &GetIncident{}
+
+ t.Run("valid configuration with incidentId", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "incidentId": "abc123-def456",
+ },
+ })
+
+ require.NoError(t, err)
+ })
+
+ t.Run("missing incidentId returns error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{},
+ })
+
+ require.ErrorContains(t, err, "incidentId is required")
+ })
+
+ t.Run("empty incidentId returns error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "incidentId": "",
+ },
+ })
+
+ require.ErrorContains(t, err, "incidentId is required")
+ })
+
+ t.Run("invalid configuration format -> decode error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: "invalid-config",
+ })
+
+ require.ErrorContains(t, err, "error decoding configuration")
+ })
+}
+
+func Test__GetIncident__Execute(t *testing.T) {
+ component := &GetIncident{}
+
+ t.Run("successful get emits incident", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(`{
+ "data": {
+ "id": "inc-uuid-123",
+ "type": "incidents",
+ "attributes": {
+ "title": "Database connection issues",
+ "sequential_id": 42,
+ "slug": "database-connection-issues",
+ "summary": "Users are experiencing slow database queries.",
+ "status": "started",
+ "severity": "sev1",
+ "started_at": "2026-01-19T12:00:00Z",
+ "updated_at": "2026-01-19T12:00:00Z",
+ "url": "https://app.rootly.com/incidents/inc-uuid-123"
+ }
+ }
+ }`)),
+ },
+ },
+ }
+
+ integrationCtx := &contexts.IntegrationContext{
+ Configuration: map[string]any{
+ "apiKey": "test-api-key",
+ },
+ }
+
+ execState := &contexts.ExecutionStateContext{
+ KVs: make(map[string]string),
+ }
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "incidentId": "inc-uuid-123",
+ },
+ HTTP: httpContext,
+ Integration: integrationCtx,
+ ExecutionState: execState,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, execState.Passed)
+ assert.Equal(t, core.DefaultOutputChannel.Name, execState.Channel)
+ assert.Equal(t, "rootly.incident", execState.Type)
+ assert.Len(t, execState.Payloads, 1)
+
+ // Verify request
+ require.Len(t, httpContext.Requests, 1)
+ req := httpContext.Requests[0]
+ assert.Equal(t, http.MethodGet, req.Method)
+ assert.Contains(t, req.URL.String(), "/incidents/inc-uuid-123")
+ assert.Equal(t, "application/vnd.api+json", req.Header.Get("Content-Type"))
+ })
+
+ t.Run("API error returns error and does not emit", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusNotFound,
+ Body: io.NopCloser(strings.NewReader(`{"errors": [{"title": "Record not found"}]}`)),
+ },
+ },
+ }
+
+ integrationCtx := &contexts.IntegrationContext{
+ Configuration: map[string]any{
+ "apiKey": "test-api-key",
+ },
+ }
+
+ execState := &contexts.ExecutionStateContext{
+ KVs: make(map[string]string),
+ }
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "incidentId": "nonexistent-id",
+ },
+ HTTP: httpContext,
+ Integration: integrationCtx,
+ ExecutionState: execState,
+ })
+
+ require.Error(t, err)
+ assert.ErrorContains(t, err, "failed to get incident")
+ assert.False(t, execState.Passed)
+ assert.Empty(t, execState.Channel)
+ })
+}
diff --git a/pkg/integrations/rootly/rootly.go b/pkg/integrations/rootly/rootly.go
index cf32d97772..34790b4083 100644
--- a/pkg/integrations/rootly/rootly.go
+++ b/pkg/integrations/rootly/rootly.go
@@ -64,6 +64,7 @@ func (r *Rootly) Components() []core.Component {
&CreateIncident{},
&CreateEvent{},
&UpdateIncident{},
+ &GetIncident{},
}
}
diff --git a/web_src/src/pages/workflowv2/mappers/rootly/get_incident.ts b/web_src/src/pages/workflowv2/mappers/rootly/get_incident.ts
new file mode 100644
index 0000000000..ae52c49f8a
--- /dev/null
+++ b/web_src/src/pages/workflowv2/mappers/rootly/get_incident.ts
@@ -0,0 +1,63 @@
+import { ComponentBaseProps } from "@/ui/componentBase";
+import { getBackgroundColorClass } from "@/utils/colors";
+import { getStateMap } from "..";
+import {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../types";
+import { MetadataItem } from "@/ui/metadataList";
+import rootlyIcon from "@/assets/icons/integrations/rootly.svg";
+import { Incident } from "./types";
+import { baseEventSections, getDetailsForIncident } from "./base";
+import { formatTimeAgo } from "@/utils/date";
+
+export const getIncidentMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ const lastExecution = context.lastExecutions.length > 0 ? context.lastExecutions[0] : null;
+ const componentName = context.componentDefinition.name || "unknown";
+
+ return {
+ iconSrc: rootlyIcon,
+ collapsedBackground: getBackgroundColorClass(context.componentDefinition.color),
+ collapsed: context.node.isCollapsed,
+ title:
+ context.node.name ||
+ context.componentDefinition.label ||
+ context.componentDefinition.name ||
+ "Unnamed component",
+ eventSections: lastExecution ? baseEventSections(context.nodes, lastExecution, componentName) : undefined,
+ metadata: metadataList(context.node),
+ includeEmptyState: !lastExecution,
+ eventStateMap: getStateMap(componentName),
+ };
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default: OutputPayload[] };
+ if (!outputs?.default || outputs.default.length === 0) {
+ return {};
+ }
+ const incident = outputs.default[0].data as Incident;
+ return getDetailsForIncident(incident);
+ },
+
+ subtitle(context: SubtitleContext): string {
+ if (!context.execution.createdAt) return "";
+ return formatTimeAgo(new Date(context.execution.createdAt));
+ },
+};
+
+function metadataList(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const configuration = node.configuration as Record;
+
+ if (configuration?.incidentId) {
+ metadata.push({ icon: "alert-triangle", label: `Incident: ${configuration.incidentId}` });
+ }
+
+ return metadata;
+}
diff --git a/web_src/src/pages/workflowv2/mappers/rootly/index.ts b/web_src/src/pages/workflowv2/mappers/rootly/index.ts
index a02e55f3fd..27e8a8e439 100644
--- a/web_src/src/pages/workflowv2/mappers/rootly/index.ts
+++ b/web_src/src/pages/workflowv2/mappers/rootly/index.ts
@@ -3,12 +3,14 @@ import { onIncidentTriggerRenderer } from "./on_incident";
import { createIncidentMapper } from "./create_incident";
import { createEventMapper } from "./create_event";
import { updateIncidentMapper } from "./update_incident";
+import { getIncidentMapper } from "./get_incident";
import { buildActionStateRegistry } from "../utils";
export const componentMappers: Record = {
createIncident: createIncidentMapper,
createEvent: createEventMapper,
updateIncident: updateIncidentMapper,
+ getIncident: getIncidentMapper,
};
export const triggerRenderers: Record = {
@@ -19,4 +21,5 @@ export const eventStateRegistry: Record = {
createIncident: buildActionStateRegistry("created"),
createEvent: buildActionStateRegistry("created"),
updateIncident: buildActionStateRegistry("updated"),
+ getIncident: buildActionStateRegistry("retrieved"),
};