A Squadron plugin that integrates Devin AI for automated pull request QA, code review, and code development.
This plugin uses the Devin v3 API.
Every session-creating tool (code_qa, code_review, code_develop) is synchronous: it
creates the session, polls until Devin finishes, and returns one text result. So a mission stage
that calls code_develop blocks for the length of the Devin session — set
poll_timeout_minutes to cover the longest task you expect (a multi-hour development session
needs far more than the 60-minute default).
code_develop ──create──> Devin session ──poll every 15s──> done
│ │
│ archive_on_complete = true → archived, terminal
│ archive_on_complete = false → left open
▼ ▼
session_id ─────────────────────> send_message (iterate, blocks again)
│
▼
complete_session (archive)
check_session is the read-only view of any session by ID — use it to inspect a session another
stage created, or one whose result you no longer have. find_sessions is how you get that ID
without being told it: search by the ticket tag the earlier stage created its session with.
Every result is plain text with these sections, in order:
=== Devin Development Complete ===
Session: <id> URL: <session url> Status: <status>
Pull Requests: <pr url> (<state>)
--- Structured Output --- the session's structured_output JSON, verbatim
--- Devin's Response --- Devin's final message, plus a link to the full transcript
The Structured Output and Pull Requests sections are omitted when the session has neither.
This is the part a mission should route on; the prose response is for humans reading the run.
It comes from Devin, not from this plugin: when a session runs a playbook that defines a
structured_output_schema, Devin populates structured_output on the session, and the plugin
reads it back from GET /v3/organizations/{org_id}/sessions/insights (or from the v1 session, if
that read is refused) and prints it verbatim in every result (not just check_session). A mission
then parses those fields for its router conditions, e.g.:
# the investigation playbook's schema emits { "verdict": "...", "evidence_complete": true }
router {
condition = "verdict == 'DEFECT_PROVEN'"
send_to = task.develop
}If a session has no playbook schema, structured_output is empty and the section is absent — the
stage has to fall back to reading Devin's final message, which is exactly the ambiguity the
schemas exist to remove.
Everything goes through five organization-scoped v3 endpoints on https://api.devin.ai/v3
(devin/client.go), authenticated with Authorization: Bearer <api_key>:
| Call | Endpoint | Used for |
|---|---|---|
CreateSession |
POST /organizations/{org}/sessions |
{prompt, repos, title, tags}. repos is how code_develop grants repo access; title/tags are metadata only and never reach the prompt. |
GetSession |
GET .../sessions/{id} |
poll target: status, status_detail, pull_requests |
GetMessages |
GET .../sessions/{id}/messages |
the transcript, returned as raw JSON |
GetSessionInsights |
GET .../sessions/insights?session_ids={id} |
structured_output plus Devin's analysis (issues, action items, timeline) |
SendMessage / ArchiveSession |
POST .../messages, POST .../archive |
resume, and finalize |
find_sessions is the one exception to "everything is v3": it calls
GET https://api.devin.ai/v1/sessions?tags=<tag>&tags=<tag>&limit=<n>, because v1 is where tag
filtering is documented, while the v3 list endpoint takes an undocumented qs object. Same bearer
key; the organization is implied by the key rather than being in the path. The list response is a
summary — no structured output — so check_session is still what fetches a session's detail.
The organization-scoped paths above are gated on Devin's ViewOrgSessions / ManageOrgSessions
permissions, which a service user with the default Member role does not hold — it can create
sessions and read its own, but the /v3/organizations/{org}/... reads are refused with a 403 even
for a session it created itself. So each of the three v3 reads retries against the v1 per-session
endpoint, GET /v1/sessions/{id}, which is scoped to what the key can see rather than to the
organization:
| Refused v3 read | Retried as | Caveat |
|---|---|---|
GetSession |
GET /v1/sessions/{id} |
v1 reports the lifecycle in status_enum (working, blocked, finished, expired, ...), which is mapped onto status; the poller matches both vocabularies. |
GetSessionInsights |
same call, structured_output field |
the routable field survives; Devin's analysis (issues, action items, timeline) does not exist in v1 and is lost. |
GetMessages |
same call, messages field |
shape differs from the v3 transcript; lastDevinMessage is forgiving enough for both. |
A 403 is retried, any other failure is returned as-is: falling back on a 500 would hide a real outage behind a second call that fails the same way.
find_sessions has no such fallback, and this is the one capability a Member service user
cannot recover on its own: listing is inherently an organization-wide read, and no self-scoped list
endpoint exists. Its optional user_email narrows the search to one account's sessions, which is
worth trying with the service user's own address — an org-wide search refused unrestricted may be
permitted when scoped to the caller — but that is untested against a Member key. On a refusal the
tool returns an error that says so explicitly, because the failure mode to avoid is a caller
reading "refused" as "this ticket has no sessions" and starting duplicate work. A workflow that
needs discovery under a Member key has to record the session ids it creates in its own state and
read them back through check_session.
Polling. PollUntilDone ticks every 15s until a terminal state, and tolerates 5 consecutive
transient GetSession failures before giving up (the counter resets on any success), so a brief
API blip doesn't kill a long session. Terminal means either status in
exit | error | suspended | sleeping | waiting_for_user, or status_detail in
waiting_for_user | finished while status is still running — that second case is the normal
end of a successful session, since Devin stays running and awaits follow-up. blocked | expired | stopped count too, since a status served by the v1 fallback speaks v1's vocabulary and an
unrecognised terminal state polls to the timeout. Hitting
poll_timeout_minutes is an error, not a result: the session keeps going on Devin's side, so
recover it with check_session rather than re-running the stage.
A tool result is assembled from those calls in order — header and Pull Requests from
GetSession, Structured Output from GetSessionInsights, Devin's Response from
GetMessages. Failures downgrade rather than abort: a failed GetMessages prints the error plus
the session URL, and absent insights simply omit their section, so a stage still gets the session
ID and PR links. Each of those calls falls back to v1 before it counts as failed, per
Permissions, and the v1 fallback.
Message extraction. The messages payload is not a stable shape, so lastDevinMessage is
deliberately forgiving: it accepts either a bare array or {"messages": [...]}, walks backwards
to the last Devin-authored entry (a session ends with Devin's summary unless it stopped to ask a
question, in which case that question is what you want), sniffs authorship across
type/origin/role/source/author, and reads the body from whichever of
message/text/content/body is populated. Any entry with no recognizable authorship field is
treated as Devin's, and an unparseable payload falls back to printing the raw JSON — both
biased toward showing you something over silently dropping what Devin said.
code_develop builds the prompt for you. In default mode it appends a fixed workflow — create a
branch, implement, add tests, commit, open a PR — which is right for ordinary development and
wrong for two common stages: a read-only investigation (must not branch or open a PR) and a
follow-on stage that must push to a branch and PR that already exist. prompt_mode = "raw"
sends task (and branch/instructions, if given) verbatim, so the task text has to carry every
instruction the job needs — including what not to do.
Performs a full QA review of a pull request. Devin checks out the PR branch, analyzes changes, runs existing tests, and returns a comprehensive summary.
The QA review covers:
- Bug detection, edge cases, and logic errors
- Error handling adequacy
- Test execution and failure reporting
- Missing test coverage for new/changed code
- Regression risks in related functionality
- Alignment with PR description and linked issues
- Performance concerns
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
pr_url |
string | yes | Full URL of the GitHub PR (e.g. https://github.com/org/repo/pull/123) |
instructions |
string | no | Additional instructions or focus areas for the QA review |
title |
string | no | Title for the Devin session. Devin generates one if omitted. |
tags |
string[] | no | Tags to apply to the Devin session, for filtering sessions later |
Performs a full code review of a pull request. Devin reviews the diff, posts inline comments directly on the GitHub PR, and submits an overall review summary.
The code review covers:
- Every changed file in the PR diff
- Code quality, readability, and maintainability
- Correctness and potential bugs
- Security concerns and vulnerabilities
- Adherence to best practices and coding conventions
- Improvement suggestions
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
pr_url |
string | yes | Full URL of the GitHub PR (e.g. https://github.com/org/repo/pull/123) |
instructions |
string | no | Additional instructions or focus areas for the code review |
title |
string | no | Title for the Devin session. Devin generates one if omitted. |
tags |
string[] | no | Tags to apply to the Devin session, for filtering sessions later |
Develops code on a repository. Devin clones the repo, implements the requested changes, runs tests, and opens a pull request with the completed work.
Use this for:
- Feature development
- Bug fixes
- Refactoring
- Any code changes on a repository
Devin will follow existing code conventions, add or update tests, and open a PR with a detailed description. The repository is passed to Devin via the v3 repos field, so Devin has direct access to it.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
repo_url |
string | yes | Full URL of the GitHub repository (e.g. https://github.com/org/repo) |
task |
string | yes | Description of the development task to perform |
branch |
string | no | Branch name for Devin to create. If omitted, Devin chooses an appropriate name. |
instructions |
string | no | Additional context, constraints, or coding guidelines |
title |
string | no | Title for the Devin session (e.g. DEV-8126 investigate). Devin generates one if omitted. |
tags |
string[] | no | Tags to apply to the Devin session (e.g. ["ratevariant", "investigate"]), for filtering sessions later |
prompt_mode |
string | no | default (default) wraps the task in the branch / tests / commit / PR workflow above. raw sends task and instructions verbatim with no added steps. |
Use prompt_mode = "raw" when the default workflow is wrong for the job: a read-only
investigation that must not create a branch or PR, or a later stage that must push to a branch
and PR that already exist. In raw mode the task text is the whole prompt, so it has to carry
its own instructions.
Checks the status of an existing Devin session. Returns the full session status including current state, pull requests, and Devin's messages. Use this to inspect a session that was previously created by another tool or to check on a long-running session.
The session ID is returned by code_qa, code_review, and code_develop when they create a session.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | yes | The Devin session ID (e.g. 32fee96e7997499ca010301aa50eefce) |
The result also carries Last Activity and, when the session is past it, the 30-day resume
window — see below.
Sends a follow-up message to an existing Devin session and waits for Devin to finish responding. Use this to continue a conversation with a session that is waiting for user input, for example to answer a question, give additional instructions, or request changes.
The session must still be open (not archived) and less than 30 days old. Devin does not
continue a session after that, and nothing in its status says so: an expired session reads
suspended / inactivity exactly like one suspended an hour ago, so a stage that routes on status
alone briefs a session that will never read the message. check_session therefore prints
Last Activity: 2026-07-28T09:50:00Z (43 days ago)
Resumable: NO — Devin does not continue a session more than 30 days after its last activity...
from the session's updated_at (falling back to created_at; v3 sends epoch seconds and v1 sends
RFC 3339, both decode). A session whose timestamps are absent reads as resumable — the refused send
is then the evidence, which beats declaring a live session dead. Past the window, carry the
findings into a new session rather than resuming.
To keep sessions resumable after code_qa, code_review, or code_develop complete, set archive_on_complete = "false" in the plugin settings.
After sending the message, the plugin reuses the same polling logic as the other tools, waiting until Devin finishes the follow-up work before returning its response.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | yes | The Devin session ID to send the message to |
message |
string | yes | The message to send to Devin (follow-up instruction, answer, or change request) |
Finds existing sessions by tag. This is how a mission discovers what an earlier run already did
for a ticket without a human passing session IDs in: every session-creating tool takes tags, so
tagging sessions with the ticket key (["DEV-8126", "rate-investigation"]) makes them findable
later by that key. A session must carry all the given tags to match, and matching is exact.
The result is one block per session — ID, status, title, PR link, timestamps, tags, session URL —
intended for choosing which session to act on, not for reading its work: follow up with
check_session for detail (structured output, transcript) or send_message to continue it. No
match is a normal answer, reported as Matches: 0.
Unlike the other tools this one is a single API call and returns immediately — it creates nothing and does not poll.
It is also the one tool a Member service user may not be able to run at all, since listing sessions
is an organization-wide read with no self-scoped equivalent to fall back to (see
Permissions, and the v1 fallback). A refusal comes back as an
error naming the permission, never as Matches: 0 — the two must not be confused, because reading
"refused" as "no prior sessions" is what makes a workflow start work that is already underway.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
tags |
string[] | yes | Tags a session must all carry to match |
limit |
number | no | Maximum sessions to return. Defaults to 20. |
user_email |
string | no | Restrict to sessions created by this account. Worth trying with the service user's own address when the unrestricted search is refused. |
Finalizes and archives a Devin session. Call this upon mission finalization once no further follow-up messages are needed, to archive the session and release its resources. After completion the session can no longer be resumed with send_message.
This is the explicit counterpart to archive_on_complete = "false": when sessions are left resumable, use complete_session to archive them when the work is truly done.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | yes | The Devin session ID to finalize and archive |
- Go 1.23+
- A Devin AI account with v3 API access
- A Devin service user with an API key (starts with
cog_) - Your Devin organization ID
- Devin must already have access to the GitHub repos you want to review or develop on
The v3 API uses service user tokens instead of personal API keys. Follow these steps to get set up:
-
Create a service user in your Devin organization:
- Go to your Devin dashboard at app.devin.ai
- Navigate to Settings > Service Users
- Create a new service user and assign it a role with the
UseDevinSessionspermission
-
Copy the API key — it starts with
cog_and is only shown once. Store it securely. -
Find your organization ID — visible on the Settings > Service Users page in the Devin dashboard.
-
Grant repo access — ensure Devin has access to the GitHub repositories you want to work with. This is configured in your Devin organization's GitHub integration settings.
For more details, see the Devin API documentation.
# Clone the plugin project
git clone git@github.com:FedTax/squadron-plugin-devin.git
# Build
cd squadron-plugin-devin
go mod tidy
go build -o plugin .
# Install into Squadron's plugin directory
mkdir -p ~/.squadron/plugins/devin/local
cp plugin ~/.squadron/plugins/devin/local/pluginAdd the plugin to your Squadron HCL config:
plugin "devin" {
# A released version resolves to a git tag in the repo named by `source`:
# source = "github.com/FedTax/squadron-plugin-devin"
# version = "v0.0.4"
# `version = "local"` instead uses the binary installed under
# ~/.squadron/plugins/devin/local (see Build above).
version = "local"
settings = {
api_key = "<your-devin-service-user-key>"
org_id = "<your-devin-org-id>"
poll_timeout_minutes = "60"
archive_on_complete = "true"
}
}Then attach the tools to an agent:
agent "reviewer" {
model = models.anthropic.claude_sonnet_4
tools = [plugins.devin.code_qa, plugins.devin.code_review, plugins.devin.code_develop, plugins.devin.find_sessions, plugins.devin.check_session, plugins.devin.send_message, plugins.devin.complete_session]
}| Setting | Required | Description |
|---|---|---|
api_key |
yes | Devin service user API key (starts with cog_). Created under Settings > Service Users in the Devin dashboard. |
org_id |
yes | Devin organization ID. Found on the Settings > Service Users page in the Devin dashboard. |
poll_timeout_minutes |
no | Maximum time in minutes to wait for a Devin session to complete. Defaults to 60. Increase for long-running development tasks. |
archive_on_complete |
no | Whether code_qa, code_review, and code_develop archive their session once Devin finishes. Defaults to true. Set to false to leave sessions resumable so they can be continued with send_message and finalized with complete_session. |
raw_messages |
no | Whether tool results carry the session's entire messages JSON payload. Defaults to false: results carry Devin's final message, the session's structured output, and PR links. Set to true for the full transcript, which is large enough to crowd out the rest of the caller's context. |
- The agent invokes a tool (
code_qa,code_review, orcode_develop) with the required parameters. - The plugin creates a new Devin session via the Devin v3 API (
POST /v3/organizations/{org_id}/sessions). - The plugin polls the session status every 15 seconds (up to
poll_timeout_minutes, default 60) until Devin finishes. - The session is archived (unless
archive_on_complete = "false") and the result is returned as the text summary described in What a tool result contains.
When archive_on_complete is false, sessions are left open after they finish. Use send_message to continue working with a session (the plugin sends the message and polls until Devin finishes again), and complete_session to archive it once the mission is finalized.
For code_review, Devin also posts inline review comments directly on the GitHub PR during its session.
For code_develop, the repository URL is passed via the v3 repos field so Devin has direct access. Devin implements the changes and opens a pull request. The PR link is included in the result.
For check_session, the plugin fetches the current status and message history for an existing session without creating a new one. This is useful for inspecting sessions created by other tools or checking on long-running work.
All tools respect context cancellation, so Squadron can terminate long-running sessions cleanly. Transient API errors during polling are retried automatically (up to 5 consecutive failures).
squadron-plugin-devin/
main.go # Entry point - registers the plugin with Squadron
plugin.go # ToolProvider implementation, tool definitions, prompt builders
devin/
client.go # HTTP client for the Devin AI v3 API
go.mod
go.sum
See LICENSE for details.