Skip to content

feat: Add Linear integration#3041

Open
brainstormingdev-super wants to merge 1 commit intosuperplanehq:mainfrom
brainstormingdev-super:feat/linear-integration
Open

feat: Add Linear integration#3041
brainstormingdev-super wants to merge 1 commit intosuperplanehq:mainfrom
brainstormingdev-super:feat/linear-integration

Conversation

@brainstormingdev-super
Copy link

Implements #2485

Description

This PR implements the Linear integration for SuperPlane with 2 components:

  • OnIssueCreated (trigger): Activates workflows when Linear issues are created, with optional filtering by team and/or labels. Uses Linear webhooks with HMAC-SHA256 signature verification.
  • CreateIssue (action): Creates new Linear issues with configurable team, title, description, assignee, labels, priority, and status fields.

Authentication is via personal API token, which users can generate in Linear at Settings > API > Personal API keys. The integration uses Linear's GraphQL API exclusively for all operations (teams, labels, issue creation, webhook management).

On setup, the integration syncs available teams and labels from Linear, which are then used to populate resource pickers in the UI.

Changes

Backend (pkg/integrations/linear/)

  • linear.go — Integration entry point, API token auth, metadata sync (teams/labels)
  • client.go — GraphQL API client (viewer, teams, labels, issue create, webhook create/delete)
  • common.go — Shared data models (Team, Label, Issue)
  • on_issue_created.go — Webhook-based trigger with team/label filtering
  • create_issue.go — Issue creation action with full field support
  • webhook_handler.go — Webhook lifecycle (setup, config comparison, cleanup)
  • list_resources.go — Resource discovery for UI pickers
  • example.go — Embedded example payloads

Frontend (web_src/src/pages/workflowv2/mappers/linear/)

  • Trigger renderer and component mapper following existing patterns
  • Icon and sidebar registration

Docs

  • docs/components/Linear.mdx — Generated component documentation

Tests

  • linear_test.go — Integration sync (missing token, invalid creds, successful sync)
  • client_test.go — Client creation and API calls
  • create_issue_test.go — Setup validation and execution
  • on_issue_created_test.go — Signature verification, event filtering, webhook setup
  • webhook_handler_test.go — Config comparison logic

[video-demo]

@AleksandarCole AleksandarCole added bounty This issue has a bounty open pr:stage-1/3 Needs to pass basic review. pr:stage-2/3 Needs to pass functional review and removed pr:stage-1/3 Needs to pass basic review. labels Feb 11, 2026
@brainstormingdev-super brainstormingdev-super force-pushed the feat/linear-integration branch 2 times, most recently from 151ded4 to 435e1e7 Compare February 11, 2026 17:24
@brainstormingdev-super brainstormingdev-super force-pushed the feat/linear-integration branch 2 times, most recently from 49fc617 to 81243e8 Compare February 11, 2026 18:26
@zekebawt
Copy link

🤖 Claiming this bounty! Will implement Linear integration with OnIssueCreated trigger and CreateIssue action. ETA: 2-3 hours.

@AleksandarCole
Copy link
Collaborator

@brainstormingdev-super Looks good overall and works as expected. Here are some small UX changes we need to address before we move to code review:

  • createIssue uses wrong input fields for title/description/assignee - they are the expression fields that filter/if use, but instead they should be input with expression supported like Wait for in the wait component for example.
  • Let's clean up the details tab for both components - it should show timestamp at the top (Created at:) also, let's remove things like TeamID, StateID, or IDs in general. In the create issue details tab, make sure to include link to the issue - like what we have in onIssueCreated.

Signed-off-by: Andrew <cool.dev12701@gmail.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

}
}
return true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asymmetric set comparison in resourceTypesEqual

Low Severity

resourceTypesEqual only verifies that every element of b exists in a, but not the reverse. Combined with the length check, this is only correct when neither slice contains duplicates. If a = ["Issue", "Comment"] and b = ["Issue", "Issue"], the function incorrectly returns true — the lengths match and both elements of b are found in a, but the slices represent different sets. Using slices.Sort + slices.Equal or checking both directions would fix this.

Fix in Cursor Fix in Web

@brainstormingdev-super
Copy link
Author

@AleksandarCole I’ve improved the UX. Please take a look at the video demo below.
video demo

@AleksandarCole AleksandarCole added pr:stage-3/3 Ready for full, in-depth, review and removed pr:stage-2/3 Needs to pass functional review labels Feb 13, 2026
@AleksandarCole
Copy link
Collaborator

@brainstormingdev-super looks good - moved to code review.

@shiroyasha
Copy link
Collaborator

Hey @brainstormingdev-super, tried it out, works good.

When looking at Linear, OAuth seems to be the default way to integrate with the system.
Why did you decide to go with the personal API key?

I see two big problems with Personal API key vs OAuth:

1/ When you create an issue with personal keys, the creator is the Personal Key's owner. With OAuth the creator of the issue can be a SuperPlaneBot which is more natural for integrations and workflow automations.
2/ When the user leaves the organization, the SuperPlane automation will no longer work.

cc @AleksandarCole

Returning to stage-2

@shiroyasha shiroyasha added pr:stage-2/3 Needs to pass functional review and removed pr:stage-3/3 Ready for full, in-depth, review labels Feb 13, 2026

const onIssueCreatedPayloadType = "linear.issue.create"

type OnIssueCreated struct{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AleksandarCole I don't think we should have a onIssueCreated trigger. Instead, we should have a onIssue trigger, that allows you select the action you're interested in (create, close, deleted, ...). This pattern is already used in the github.onIssue and gitlab.onIssue

Comment on lines +122 to +139
func (c *Client) ListTeams() ([]Team, error) {
const query = `query($after: String) { teams(first: 100, after: $after) { nodes { id name key } pageInfo { hasNextPage endCursor } } }`
var all []Team
var cursor *string
for {
vars := map[string]any{"after": cursor}
var out teamsResponse
if err := c.execGraphQL(query, vars, &out); err != nil {
return nil, err
}
all = append(all, out.Teams.Nodes...)
if !out.Teams.PageInfo.HasNextPage {
break
}
cursor = &out.Teams.PageInfo.EndCursor
}
return all, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor concern: if hasNextPage stays true and endCursor doesn’t change (e.g., due to an API inconsistency or pagination bug), this could loop forever. Maybe add a cursor-change check or safety cap?

Comment on lines +1 to +26
{
"action": "create",
"type": "Issue",
"data": {
"id": "2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9",
"createdAt": "2020-01-23T12:53:18.084Z",
"updatedAt": "2020-01-23T12:53:18.084Z",
"title": "Example issue",
"description": "Issue description",
"identifier": "ENG-42",
"teamId": "72b2a2dc-6f4f-4423-9d34-24b5bd10634a",
"stateId": "state-1",
"priority": 2,
"labelIds": ["label-1"],
"assigneeId": "user-1"
},
"actor": {
"id": "b5ea5f1f-8adc-4f52-b4bd-ab4e84cf51ba",
"type": "user",
"name": "Alice",
"email": "alice@example.com"
},
"url": "https://linear.app/company/issue/ENG-42/example-issue",
"createdAt": "2020-01-23T12:53:18.084Z",
"webhookTimestamp": 1676056940508
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the data that appears on trigger payload (in the sidebar event). Check it and replace it here with mocked data

@brainstormingdev-super
Copy link
Author

brainstormingdev-super commented Feb 14, 2026

Hey @brainstormingdev-super, tried it out, works good.

When looking at Linear, OAuth seems to be the default way to integrate with the system. Why did you decide to go with the personal API key?

I see two big problems with Personal API key vs OAuth:

1/ When you create an issue with personal keys, the creator is the Personal Key's owner. With OAuth the creator of the issue can be a SuperPlaneBot which is more natural for integrations and workflow automations. 2/ When the user leaves the organization, the SuperPlane automation will no longer work.

cc @AleksandarCole

Returning to stage-2

@shiroyasha, I will update to implement OAuth2.
Btw, I don’t think we can automatically register a webhook for the OnIssueCreate trigger. It looks like webhook creation requires admin permissions, and Linear doesn’t allow the admin scope when actor=app. So we probably need a workspace admin to create the webhook manually.
cc @AleksandarCole

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bounty This issue has a bounty open pr:stage-2/3 Needs to pass functional review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants