linear-sdk-scripting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Linear SDK Scripting

Linear SDK 脚本编写

Drive Linear through the official TypeScript SDK (
@linear/sdk
) by writing throwaway Node scripts and executing them. This replaces the Linear MCP server: anything the MCP can do (read issues, create issues, comment, change status, query teams/projects/cycles) you do here by calling SDK methods.
The SDK is preferred over hand-written GraphQL because pagination, relationship traversal, and mutation payloads are normalized, and method and input names are predictable. When you need a field or filter you do not know, consult the docs index in
references/docs-index.md
and fetch the specific page rather than guessing.
通过编写一次性Node脚本并执行,借助官方TypeScript SDK(
@linear/sdk
)来操作Linear。这可以替代Linear MCP服务器:MCP能完成的所有操作(读取工单、创建工单、添加评论、更改状态、查询团队/项目/周期),你都可以通过调用SDK方法在此实现。
相较于手写GraphQL,SDK更为推荐,因为它对分页、关系遍历和变更负载进行了规范化,且方法和输入名称具有可预测性。当你需要某个未知的字段或过滤器时,请查阅
references/docs-index.md
中的文档索引,获取具体页面内容,而非自行猜测。

Workflow at a glance

概览工作流

Be reactive: try the script first, recover on auth failure, and only create a key as a last resort. Do not gate on
$LINEAR_API_KEY
being set in the current shell, that variable is almost always empty here even when a valid key is already persisted (see Setup).
  1. Make sure the SDK is installed in the working dir. See Setup.
  2. Write a small
    .mjs
    script into the working dir that imports
    LinearClient
    and does the task.
  3. Run it with
    node
    , sourcing the shell profiles first so a persisted key is picked up (see Execution pattern).
  4. If it fails with a 401 /
    AuthenticationLinearError
    , source the shell profiles and retry once. Only if it still fails is the key actually missing or invalid: run the API key setup flow with the user.
灵活应对:先尝试运行脚本,在认证失败时进行恢复,仅在万不得已时才创建密钥。不要以当前shell中是否设置了
$LINEAR_API_KEY
作为判断依据,即使已持久化有效密钥,此变量在此处几乎始终为空(请参阅设置部分)。
  1. 确保工作目录中已安装SDK。请参阅设置部分。
  2. 在工作目录中编写一个小型
    .mjs
    脚本,导入
    LinearClient
    并完成任务。
  3. 使用
    node
    运行脚本,先加载shell配置文件,以便读取已持久化的密钥(请参阅执行模式)。
  4. 如果因401错误/
    AuthenticationLinearError
    导致失败,加载shell配置文件并重试一次。只有当再次失败时,才说明密钥确实缺失或无效:引导用户完成API密钥设置流程。

Setup

设置

1. Personal API key

1. 个人API密钥

The SDK authenticates with a Linear personal API key read from the
LINEAR_API_KEY
environment variable.
Do not decide whether a key exists by checking the env var up front. Scripts here run in a non-interactive, non-login shell that does not source
~/.zshrc
,
~/.zshenv
,
~/.bashrc
, etc., so
$LINEAR_API_KEY
reads as empty even when a valid key is already persisted in one of those files. An empty variable in this shell does not mean the key is unconfigured.
Instead, let the script attempt the work (the Execution pattern sources the profiles first), and only treat the key as missing if it still auth-fails after that. See Handling auth failures.
Only when the key is genuinely missing, guide the user through creating one:
  1. Open Security and access settings: https://linear.app/settings/account/security
  2. Under Personal API keys, create a new key. Copy it (it starts with
    lin_api_
    ).
  3. Ask the user to paste their key, then load it into the current session so you can use it immediately without a new terminal:
    sh
    export LINEAR_API_KEY="lin_api_REPLACE_ME"
  4. Ask before persisting. The key is a secret and persisting it writes to a file the user owns, so do not do it on your own initiative. Explicitly ask first, for example: "Do you want me to persist this key to your shell profile (
    ~/.zshrc
    ) so it's available in future sessions?" Wait for a clear yes.
    Only after the user confirms, perform the write yourself using the command for the user's shell:
    • zsh:
      sh
      echo 'export LINEAR_API_KEY="lin_api_REPLACE_ME"' >> ~/.zshrc
    • bash (or
      ~/.bash_profile
      on macOS login shells):
      sh
      echo 'export LINEAR_API_KEY="lin_api_REPLACE_ME"' >> ~/.bashrc
    • fish:
      sh
      echo 'set -gx LINEAR_API_KEY "lin_api_REPLACE_ME"' >> ~/.config/fish/config.fish
    This explicit in-conversation approval is what makes the write a user-requested action rather than an agent-initiated one. If the user declines or does not answer, do not persist the key; it will simply need to be re-set next session.
Never print the key value back to the transcript or commit it anywhere. Treat it as a secret.
SDK通过读取
LINEAR_API_KEY
环境变量中的Linear个人API密钥进行认证。
不要通过预先检查环境变量来判断密钥是否存在。此处运行的脚本处于非交互式、非登录shell环境,不会加载
~/.zshrc
~/.zshenv
~/.bashrc
等文件,因此即使有效密钥已持久化到上述文件中,
$LINEAR_API_KEY
仍会显示为空。此shell中的变量为空并不代表密钥未配置。
相反,应让脚本尝试执行任务(执行模式会先加载配置文件),只有在加载配置文件后仍认证失败时,才判定密钥缺失。请参阅认证失败处理部分。
仅当密钥确实缺失时,才引导用户创建密钥:
  1. 打开安全与访问设置:https://linear.app/settings/account/security
  2. 在「个人API密钥」下创建新密钥。复制密钥(以
    lin_api_
    开头)。
  3. 请用户粘贴密钥,然后将其加载到当前会话中,以便无需重启终端即可立即使用:
    sh
    export LINEAR_API_KEY="lin_api_REPLACE_ME"
  4. 询问用户是否要持久化密钥。密钥属于机密信息,持久化操作会写入用户拥有的文件,因此请勿自行执行。需明确询问,例如:「是否要将此密钥持久化到你的shell配置文件(
    ~/.zshrc
    )中,以便在未来会话中可用?」等待用户明确同意。
    仅在用户确认后,才根据用户的shell类型执行写入命令:
    • zsh:
      sh
      echo 'export LINEAR_API_KEY="lin_api_REPLACE_ME"' >> ~/.zshrc
    • bash(macOS登录shell为
      ~/.bash_profile
      ):
      sh
      echo 'export LINEAR_API_KEY="lin_api_REPLACE_ME"' >> ~/.bashrc
    • fish:
      sh
      echo 'set -gx LINEAR_API_KEY "lin_api_REPLACE_ME"' >> ~/.config/fish/config.fish
    这种对话中的明确同意,使得写入操作成为用户请求的行为,而非Agent主动发起的操作。如果用户拒绝或未回应,请不要持久化密钥;下次会话时只需重新设置即可。
切勿将密钥值打印到会话记录中或提交到任何地方。将其视为机密信息。

2. Install the SDK in a working directory

2. 在工作目录中安装SDK

Keep a dedicated working directory with its own
node_modules
. Running scripts from inside it lets you use plain
import
with top-level await and no
NODE_PATH
tricks. Requires Node 18 or newer.
sh
LINEAR_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/linear-sdk-scripting"
mkdir -p "$LINEAR_DIR"
cd "$LINEAR_DIR"
[ -f package.json ] || npm init -y >/dev/null
npm ls @linear/sdk >/dev/null 2>&1 || npm install @linear/sdk
This is a one-time setup; reuse the directory afterwards.
保留一个专用工作目录,包含其自身的
node_modules
。在此目录内运行脚本,你可以使用顶层
await
的普通
import
,无需使用
NODE_PATH
技巧。需要Node 18或更高版本。
sh
LINEAR_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/linear-sdk-scripting"
mkdir -p "$LINEAR_DIR"
cd "$LINEAR_DIR"
[ -f package.json ] || npm init -y >/dev/null
npm ls @linear/sdk >/dev/null 2>&1 || npm install @linear/sdk
这是一次性设置;后续可重复使用此目录。

Execution pattern (canonical)

执行模式(标准)

For each task, write a script into the working directory and run it. Use the env var; do not inline the key.
Source the common shell profiles before invoking
node
so a persisted key is pulled into the environment (this shell does not source them automatically). This preamble is harmless when no key is persisted, so use it as the standard way to run every script:
sh
LINEAR_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/linear-sdk-scripting"
cat > "$LINEAR_DIR/task.mjs" <<'EOF'
import { LinearClient } from "@linear/sdk"

const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY })

const me = await linear.viewer
const myIssues = await me.assignedIssues({ first: 20 })
for (const issue of myIssues.nodes) {
  console.log(`${issue.identifier}  ${issue.title}`)
}
EOF
for f in ~/.zshenv ~/.zshrc ~/.zprofile ~/.profile ~/.bashrc ~/.bash_profile; do
  [ -f "$f" ] && source "$f"
done
node "$LINEAR_DIR/task.mjs"
Notes:
  • The script file lives in the working dir, so
    import "@linear/sdk"
    resolves against that dir's
    node_modules
    .
  • .mjs
    gives you top-level
    await
    , so no async wrapper is needed.
  • Emit machine-readable output (
    console.log(JSON.stringify(...))
    ) when you need to parse results in a later step.
对于每个任务,在工作目录中编写脚本并运行。使用环境变量;不要将密钥内联到脚本中。
在调用
node
前加载通用shell配置文件,以便将已持久化的密钥加载到环境中(此shell不会自动加载这些文件)。即使未持久化密钥,此预处理步骤也不会产生负面影响,因此请将其作为运行所有脚本的标准方式:
sh
LINEAR_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/linear-sdk-scripting"
cat > "$LINEAR_DIR/task.mjs" <<'EOF'
import { LinearClient } from "@linear/sdk"

const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY })

const me = await linear.viewer
const myIssues = await me.assignedIssues({ first: 20 })
for (const issue of myIssues.nodes) {
  console.log(`${issue.identifier}  ${issue.title}`)
}
EOF
for f in ~/.zshenv ~/.zshrc ~/.zprofile ~/.profile ~/.bashrc ~/.bash_profile; do
  [ -f "$f" ] && source "$f"
done
node "$LINEAR_DIR/task.mjs"
注意:
  • 脚本文件位于工作目录中,因此
    import "@linear/sdk"
    会解析此目录的
    node_modules
  • .mjs
    支持顶层
    await
    ,因此无需异步包装器。
  • 当你需要在后续步骤中解析结果时,请输出机器可读的内容(
    console.log(JSON.stringify(...))
    )。

Handling auth failures

认证失败处理

A missing or invalid key surfaces as an
AuthenticationLinearError
with
status
401. Detect it and route to the key setup flow:
js
try {
  const me = await linear.viewer
  console.log(me.displayName)
} catch (e) {
  if (e?.status === 401 || e?.constructor?.name === "AuthenticationLinearError") {
    console.error("AUTH_FAILED: set up LINEAR_API_KEY")
    process.exit(2)
  }
  throw e
}
If you see
AUTH_FAILED
(or the script crashes before any data), recover in this order rather than jumping straight to creating a key:
  1. Source the shell profiles and retry once. The key may already be persisted in a profile file that this shell never sourced. Pull it in and re-run the same script:
    sh
    for f in ~/.zshenv ~/.zshrc ~/.zprofile ~/.profile ~/.bashrc ~/.bash_profile; do
      [ -f "$f" ] && source "$f"
    done
    node "$LINEAR_DIR/task.mjs"
    (If you already ran with the canonical Execution pattern preamble, the profiles were sourced, so this retry will only help if you ran without it.)
  2. Only if it still auth-fails, the key is genuinely missing or invalid. Now do the Personal API key setup with the user (create and persist a new key), then rerun.
缺失或无效的密钥会表现为状态码401的
AuthenticationLinearError
。检测到该错误后,引导至密钥设置流程:
js
try {
  const me = await linear.viewer
  console.log(me.displayName)
} catch (e) {
  if (e?.status === 401 || e?.constructor?.name === "AuthenticationLinearError") {
    console.error("AUTH_FAILED: set up LINEAR_API_KEY")
    process.exit(2)
  }
  throw e
}
如果看到
AUTH_FAILED
(或脚本在输出任何数据前崩溃),请按以下顺序恢复,而非直接创建密钥:
  1. 加载shell配置文件并重试一次。密钥可能已持久化到某个配置文件中,但此shell从未加载过该文件。加载配置文件后重新运行同一脚本:
    sh
    for f in ~/.zshenv ~/.zshrc ~/.zprofile ~/.profile ~/.bashrc ~/.bash_profile; do
      [ -f "$f" ] && source "$f"
    done
    node "$LINEAR_DIR/task.mjs"
    (如果你已使用标准执行模式的预处理步骤运行脚本,则已加载配置文件,因此此重试仅在未使用该模式时有效。)
  2. 只有当再次认证失败时,才说明密钥确实缺失或无效。此时请引导用户完成个人API密钥设置(创建并持久化新密钥),然后重新运行脚本。

Common operations

常见操作

Concise recipes are inline below. Fuller examples (filtering, pagination loops, closing issues via workflow states, batch operations) are in
references/recipes.md
. The doc index is in
references/docs-index.md
.
Read:
js
// Current user
const me = await linear.viewer

// List issues (newest first), with a filter
const issues = await linear.issues({
  first: 25,
  filter: { state: { type: { eq: "started" } } },
})

// A single issue by UUID
const issue = await linear.issue("UUID")

// Teams, users, projects
const teams = await linear.teams()
const users = await linear.users()
const projects = await linear.projects({ first: 50 })
Write (mutations return a payload with
success
and the entity, often as a promise):
js
// Create
const created = await linear.createIssue({
  teamId: "TEAM_UUID",
  title: "Title",
  description: "Markdown body",
})
const newIssue = await created.issue

// Update (e.g. retitle, reassign)
await linear.updateIssue("ISSUE_UUID", { title: "New title", assigneeId: "USER_UUID" })

// Comment
await linear.createComment({ issueId: "ISSUE_UUID", body: "Comment text" })
To resolve human inputs to IDs (team key like
ENG
, a workflow state name like
Done
, an assignee email), look them up first. See
references/recipes.md
for the lookup-then-mutate patterns, including how to close an issue by finding the team's completed workflow state.
以下是简明的操作示例。更完整的示例(过滤、分页循环、通过工作流状态关闭工单、批量操作)位于
references/recipes.md
中。文档索引位于
references/docs-index.md
中。
读取操作:
js
// 当前用户
const me = await linear.viewer

// 列出工单(最新优先),带过滤器
const issues = await linear.issues({
  first: 25,
  filter: { state: { type: { eq: "started" } } },
})

// 通过UUID获取单个工单
const issue = await linear.issue("UUID")

// 团队、用户、项目
const teams = await linear.teams()
const users = await linear.users()
const projects = await linear.projects({ first: 50 })
写入操作(变更会返回包含
success
和实体的负载,通常为Promise):
js
// 创建工单
const created = await linear.createIssue({
  teamId: "TEAM_UUID",
  title: "Title",
  description: "Markdown body",
})
const newIssue = await created.issue

// 更新(例如重命名、重新分配)
await linear.updateIssue("ISSUE_UUID", { title: "New title", assigneeId: "USER_UUID" })

// 添加评论
await linear.createComment({ issueId: "ISSUE_UUID", body: "Comment text" })
要将人工输入转换为ID(如团队标识
ENG
、工作流状态名称
Done
、经办人邮箱),请先进行查询。有关查询后变更的模式,包括如何通过查找团队的已完成工作流状态来关闭工单,请参阅
references/recipes.md

When you need something not covered here

当你需要本文未涵盖的内容时

The Linear schema is large. Instead of guessing field or filter names:
  1. Open
    references/docs-index.md
    and pick the relevant page.
  2. Fetch that page (filtering, pagination, SDK fetching and modifying data, or the GraphQL schema reference) and use the exact names from it.
Linear的架构非常庞大。请勿猜测字段或过滤器名称:
  1. 打开
    references/docs-index.md
    并选择相关页面。
  2. 获取该页面内容(过滤、分页、SDK获取和修改数据,或GraphQL架构参考),并使用其中的准确名称。

Gotchas

注意事项

  • Run scripts from the working dir (or point at a script inside it) so
    @linear/sdk
    resolves.
    NODE_PATH
    does not resolve packages for ESM
    import
    ; it only works for CommonJS
    require
    .
  • Many SDK properties and nested relations are async and return promises or connections. Await them (
    await issue.state
    ,
    await issue.assignee
    ).
  • Connections paginate. Use
    connection.pageInfo.hasNextPage
    and
    await connection.fetchNext()
    , or iterate. See
    references/recipes.md
    .
  • Mutation results expose
    success
    and the mutated entity; the entity accessor is usually a promise (
    await payload.issue
    ).
  • 请从工作目录运行脚本(或指向目录内的脚本),以便
    @linear/sdk
    能够被解析。
    NODE_PATH
    无法解析ESM
    import
    的包;它仅适用于CommonJS
    require
  • 许多SDK属性和嵌套关系是异步的,会返回Promise或连接对象。请使用
    await
    等待结果(
    await issue.state
    await issue.assignee
    )。
  • 连接对象会分页。使用
    connection.pageInfo.hasNextPage
    await connection.fetchNext()
    ,或进行迭代。请参阅
    references/recipes.md
  • 变更结果会暴露
    success
    和变更后的实体;实体访问器通常是Promise(
    await payload.issue
    )。