forge-connector

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Forge Connector

Forge连接器

Builds a
graph:connector
Forge app that ingests external data into Atlassian's Teamwork Graph so it appears in Rovo Search and Rovo Chat.
构建一个
graph:connector
类型的Forge应用,将外部数据导入Atlassian的Teamwork Graph,使其在Rovo SearchRovo Chat中展示。

Critical Rules

核心规则

  1. Must install in Jira — Apps using Teamwork Graph modules must be installed on a Jira site. Confluence-only installs will not work.
  2. Never ask for credentials in chat — Direct users to run
    forge login
    in their own terminal.
  3. Always run the scaffold script yourself — Do not only give manual instructions; run
    scripts/scaffold_connector.py
    to generate the boilerplate.
  4. Always ask the user for their Atlassian site URL when install is needed — never discover or guess it.
  5. Atlassian deletes data on disconnect — When
    action = 'DELETED'
    , the app only needs to clean up local state; Atlassian removes the Teamwork Graph data automatically.
  6. Handler arguments are passed directly — Forge passes the request object as the first argument to handlers, NOT nested under
    event.payload
    . Config values are at
    request.configProperties
    , NOT
    event.payload.config
    . This is the most common source of
    TypeError: Cannot destructure property of undefined
    errors.
  7. Use
    @forge/kvs
    for storage
    — Import
    kvs
    from
    @forge/kvs
    . Do NOT use
    @forge/storage
    — its
    storage
    export is
    undefined
    at runtime in connector functions.
  8. Use
    graph
    named export from
    @forge/teamwork-graph
    — The correct import is
    const { graph } = require('@forge/teamwork-graph')
    . Call
    graph.setObjects({ objects, connectionId })
    . Do NOT import
    setObjects
    as a named export directly.
  9. validateConnectionHandler
    must return
    { success, message }
    — Do NOT throw an Error. Return
    { success: false, message: '...' }
    to reject,
    { success: true }
    to accept.
  10. function
    declarations belong under
    modules
    — In
    manifest.yml
    ,
    function:
    is a key under
    modules:
    , not a top-level key. Placing it at the top level causes a lint error.
  11. formConfiguration
    uses
    form
    array with
    type: header
    — Do NOT use
    fields:
    or
    beforeYouBegin:
    . The correct format uses
    form: [{ key, type: header, title, description, properties: [...] }]
    .
  12. Scopes are
    read/write/delete:object:jira
    — Use
    read:object:jira
    ,
    write:object:jira
    ,
    delete:object:jira
    . The scopes
    read:graph:teamwork
    and
    write:graph:teamwork
    are invalid and will fail
    forge lint
    .
  1. 必须安装在Jira中 — 使用Teamwork Graph模块的应用必须安装在Jira站点上。仅安装在Confluence中将无法正常工作。
  2. 切勿在聊天中索要凭据 — 引导用户在自己的终端中运行
    forge login
  3. 务必自行运行脚手架脚本 — 不要仅提供手动操作说明;运行
    scripts/scaffold_connector.py
    来生成样板代码。
  4. 当需要安装时,务必询问用户的Atlassian站点URL — 切勿自行探测或猜测。
  5. Atlassian会在断开连接时删除数据 — 当
    action = 'DELETED'
    时,应用只需清理本地状态;Atlassian会自动移除Teamwork Graph中的数据。
  6. 处理器参数直接传递 — Forge将请求对象作为第一个参数传递给处理器,而非嵌套在
    event.payload
    下。配置值位于
    request.configProperties
    ,而非
    event.payload.config
    。这是
    TypeError: Cannot destructure property of undefined
    错误最常见的原因。
  7. 使用
    @forge/kvs
    进行存储
    — 从
    @forge/kvs
    导入
    kvs
    。请勿使用
    @forge/storage
    — 其
    storage
    导出在连接器函数的运行时为
    undefined
  8. 使用
    @forge/teamwork-graph
    中的
    graph
    命名导出
    — 正确的导入方式是
    const { graph } = require('@forge/teamwork-graph')
    。调用
    graph.setObjects({ objects, connectionId })
    。请勿直接将
    setObjects
    作为命名导出导入。
  9. validateConnectionHandler
    必须返回
    { success, message }
    — 切勿抛出Error。返回
    { success: false, message: '...' }
    表示拒绝,返回
    { success: true }
    表示接受。
  10. function
    声明需放在
    modules
    — 在
    manifest.yml
    中,
    function:
    modules:
    下的一个键,而非顶层键。将其放在顶层会导致lint错误。
  11. formConfiguration
    使用包含
    type: header
    form
    数组
    — 请勿使用
    fields:
    beforeYouBegin:
    。正确格式为
    form: [{ key, type: header, title, description, properties: [...] }]
  12. 权限范围为
    read/write/delete:object:jira
    — 使用
    read:object:jira
    write:object:jira
    delete:object:jira
    。权限范围
    read:graph:teamwork
    write:graph:teamwork
    无效,会导致
    forge lint
    失败。

MCP Prerequisites

MCP前置条件

MCP ServerPurpose
Forge MCPManifest syntax, module config, deployment guides
ADS MCPAtlaskit components (only if adding Custom UI)

MCP服务器用途
Forge MCP清单语法、模块配置、部署指南
ADS MCPAtlaskit组件(仅在添加自定义UI时使用)

Agent Workflow — Complete Steps 0–5 in Order

Agent工作流 — 按顺序完成步骤0–5

Step 0: Prerequisites

步骤0:前置检查

Check Node.js (
node -v
, requires 22+), Forge CLI (
forge --version
), and login (
forge whoami
). Install missing tools:
bash
npm install -g @forge/cli
Tell the user to run
forge login
in their terminal if not authenticated.
检查Node.js(
node -v
,要求22+)、Forge CLI(
forge --version
)和登录状态(
forge whoami
)。安装缺失的工具:
bash
npm install -g @forge/cli
如果用户未认证,告知其在终端中运行
forge login

Step 1: Discover Developer Spaces

步骤1:发现开发者空间

Note:
forge developer-spaces list
does NOT exist in Forge CLI 12.x. You cannot list developer spaces non-interactively.
forge create
requires an interactive TTY to select a developer space. Ask the user to run it themselves:
Tell the user:
  cd <parent-directory>
  forge create --template blank <app-name>

  When prompted, select a Developer Space and let it complete.
  Come back when done.
The
--dev-space-id
flag in the scaffold script is optional and can be omitted — the script has been updated to skip it when not provided.
注意: Forge CLI 12.x中不存在
forge developer-spaces list
命令。无法以非交互方式列出开发者空间。
forge create
需要交互式TTY来选择开发者空间。请用户自行运行:
告知用户:
  cd <父目录>
  forge create --template blank <应用名称>

  当出现提示时,选择一个开发者空间并等待完成。
  完成后返回。
脚手架脚本中的
--dev-space-id
标记是可选的,可以省略 — 脚本已更新为在未提供该标记时跳过相关步骤。

Step 2: Scaffold the Connector App

步骤2:搭建连接器应用脚手架

Run from the skill directory (the directory containing this SKILL.md).
--dev-space-id
is optional:
bash
python3 -m scripts.scaffold_connector \
  --name <app-name> \
  --connector-name "<Human Readable Name>" \
  --object-type atlassian:document \
  --directory <parent-directory>
Add
--dev-space-id <id>
only if you have the ID from a previous step.
Object type selection — pick the type that best matches the content being ingested (see Object Types table). For mixed content, use
atlassian:document
as the default.
Form config flag — add
--has-form-config
if the admin must provide API credentials or connection details (typical for external systems). Omit it for apps that operate entirely within Atlassian (no external credentials needed).
If scaffold fails because
forge create
needs a TTY:
The scaffold script will print a manual fallback command. Have the user run
forge create
interactively, then continue from Step 3 — the scaffold script only needs to write
manifest.yml
and
src/index.js
after the directory exists.
技能目录(包含本SKILL.md的目录)运行。
--dev-space-id
为可选参数:
bash
python3 -m scripts.scaffold_connector \
  --name <应用名称> \
  --connector-name "<易读名称>" \
  --object-type atlassian:document \
  --directory <父目录>
仅当你有上一步获取的ID时,才添加
--dev-space-id <id>
对象类型选择 — 选择与要导入的内容最匹配的类型(参见对象类型表)。对于混合内容,默认使用
atlassian:document
表单配置标记 — 如果管理员必须提供API凭据或连接详情(外部系统的典型场景),则添加
--has-form-config
。对于完全在Atlassian内部运行的应用(无需外部凭据),可省略该标记。
如果脚手架因
forge create
需要TTY而失败:
脚手架脚本会打印手动回退命令。让用户交互式运行
forge create
,然后从步骤3继续 — 脚手架脚本只需在目录创建后写入
manifest.yml
src/index.js

Step 3: Customize the Generated Code

步骤3:自定义生成的代码

After scaffolding (or after the user runs
forge create
interactively):
bash
cd <app-name>
npm install
The blank template generates
src/index.js
(JavaScript, not TypeScript). Edit it to add your API calls. The scaffold generates working handler skeletons; fill in your business logic.
完成脚手架搭建(或用户交互式运行
forge create
)后:
bash
cd <应用名称>
npm install
空白模板会生成
src/index.js
(JavaScript,而非TypeScript)。编辑该文件以添加API调用。脚手架会生成可用的处理器骨架;请填充你的业务逻辑。

Key files to edit

需要编辑的关键文件

FileWhat to change
src/index.js
fetchExternalData()
— replace with your API calls
manifest.yml
Add
permissions.external.fetch.backend
URLs for any external APIs
package.json
Add
@forge/api
,
@forge/kvs
,
@forge/teamwork-graph
as dependencies
文件修改内容
src/index.js
fetchExternalData()
— 替换为你的API调用
manifest.yml
为所有外部API添加
permissions.external.fetch.backend
URL
package.json
添加
@forge/api
@forge/kvs
@forge/teamwork-graph
作为依赖

setObjects — ingest data into Teamwork Graph

setObjects — 将数据导入Teamwork Graph

Use the
graph
named export — do NOT destructure
setObjects
directly:
javascript
const { graph } = require('@forge/teamwork-graph');

const result = await graph.setObjects({
  connectionId,          // required — the connectionId from the handler request
  objects: [
    {
      schemaVersion: '1.0',
      id: 'unique-id-from-source',      // unique per connectionId
      updateSequenceNumber: 1,
      displayName: 'My Document Title',
      url: 'https://source-system.example.com/doc/123',
      createdAt: '2024-01-15T10:00:00Z',        // ISO 8601
      lastUpdatedAt: '2024-01-20T14:30:00Z',
      permissions: [{
        accessControls: [{
          principals: [{ type: 'EVERYONE' }],   // or restrict to specific users
        }],
      }],
      'atlassian:document': {
        type: {
          category: 'DOCUMENT',   // see Document Categories table below
          mimeType: 'application/vnd.google-apps.document',
        },
        content: {
          mimeType: 'application/vnd.google-apps.document',
          text: 'document title or snippet for search indexing',
        },
      },
    },
  ],
});

if (!result.success) {
  console.error('setObjects error:', result.error);
}
  • Max 100 objects per call — batch large datasets with a loop
  • id
    must be unique per
    connectionId
  • connectionId
    is required in every
    graph.setObjects()
    call
使用
graph
命名导出 — 请勿直接解构
setObjects
javascript
const { graph } = require('@forge/teamwork-graph');

const result = await graph.setObjects({
  connectionId,          // 必填 — 处理器请求中的connectionId
  objects: [
    {
      schemaVersion: '1.0',
      id: '来自数据源的唯一ID',      // 每个connectionId下唯一
      updateSequenceNumber: 1,
      displayName: '我的文档标题',
      url: 'https://source-system.example.com/doc/123',
      createdAt: '2024-01-15T10:00:00Z',        // ISO 8601格式
      lastUpdatedAt: '2024-01-20T14:30:00Z',
      permissions: [{
        accessControls: [{
          principals: [{ type: 'EVERYONE' }],   // 或限制为特定用户
        }],
      }],
      'atlassian:document': {
        type: {
          category: 'DOCUMENT',   // 参见下方文档类别表
          mimeType: 'application/vnd.google-apps.document',
        },
        content: {
          mimeType: 'application/vnd.google-apps.document',
          text: '用于搜索索引的文档标题或片段',
        },
      },
    },
  ],
});

if (!result.success) {
  console.error('setObjects错误:', result.error);
}
  • 每次调用最多100个对象 — 对于大型数据集,使用循环分批处理
  • id
    在每个
    connectionId
    下必须唯一
  • 每次
    graph.setObjects()
    调用都必须传入
    connectionId

Document Categories (for
atlassian:document.type.category
)

文档类别(用于
atlassian:document.type.category

MIME typeCategory
application/vnd.google-apps.document
DOCUMENT
application/vnd.google-apps.spreadsheet
SPREADSHEET
application/vnd.google-apps.presentation
PRESENTATION
application/vnd.google-apps.folder
FOLDER
application/pdf
PDF
image/*
IMAGE
video/*
VIDEO
audio/*
AUDIO
Other
OTHER
MIME类型类别
application/vnd.google-apps.document
DOCUMENT
application/vnd.google-apps.spreadsheet
SPREADSHEET
application/vnd.google-apps.presentation
PRESENTATION
application/vnd.google-apps.folder
FOLDER
application/pdf
PDF
image/*
IMAGE
video/*
VIDEO
audio/*
AUDIO
其他
OTHER

getObjectByExternalId — look up a single object

getObjectByExternalId — 查找单个对象

javascript
const { graph } = require('@forge/teamwork-graph');

const data = await graph.getObjectByExternalId({
  externalId: 'unique-id-from-source',
  objectType: 'atlassian:document',
  connectionId,
});
if (data.success) console.log(data.object);
javascript
const { graph } = require('@forge/teamwork-graph');

const data = await graph.getObjectByExternalId({
  externalId: '来自数据源的唯一ID',
  objectType: 'atlassian:document',
  connectionId,
});
if (data.success) console.log(data.object);

Step 4: Deploy and Install

步骤4:部署与安装

You MUST run the deploy script — do not only give the user manual
forge deploy
commands.
The deploy script lives in the forge-app-builder skill, not in this skill. Derive its directory from the path of this SKILL.md: go up two levels (
skills/forge-connector/
skills/
) then into
forge-app-builder/
. Run all commands below from that directory.
bash
undefined
你必须运行部署脚本 — 不要仅向用户提供手动
forge deploy
命令。
部署脚本位于forge-app-builder技能中,而非本技能。从本SKILL.md的路径推导其目录:向上两级(
skills/forge-connector/
skills/
)然后进入
forge-app-builder/
。从该目录运行以下所有命令。
bash
undefined

Derive forge-app-builder skill dir from this SKILL.md's path:

从本SKILL.md的路径推导forge-app-builder技能目录:

e.g. if this file is at /path/to/skills/forge-connector/SKILL.md

例如,如果本文件位于/path/to/skills/forge-connector/SKILL.md

then the deploy script dir is: /path/to/skills/forge-app-builder/

则部署脚本目录为:/path/to/skills/forge-app-builder/

If you have the site URL:

如果已有站点URL:

python3 -m scripts.deploy_forge_app
--app-dir <app-directory>
--site <site-url>
--product jira
python3 -m scripts.deploy_forge_app
--app-dir <应用目录>
--site <站点URL>
--product jira

If you don't have the site URL yet, deploy first then ask:

如果还没有站点URL,先部署再询问:

python3 -m scripts.deploy_forge_app
--app-dir <app-directory>
--product jira
--deploy-only
python3 -m scripts.deploy_forge_app
--app-dir <应用目录>
--product jira
--deploy-only

Ask: "What is your Atlassian site URL (e.g. yourcompany.atlassian.net)?"

询问:"你的Atlassian站点URL是什么(例如yourcompany.atlassian.net)?"

python3 -m scripts.deploy_forge_app
--app-dir <app-directory>
--site <site-url>
--product jira
--skip-deps
undefined
python3 -m scripts.deploy_forge_app
--app-dir <应用目录>
--site <站点URL>
--product jira
--skip-deps
undefined

Step 5: Connect via Atlassian Administration

步骤5:通过Atlassian管理后台连接

After deployment, tell the user to:
  1. Go to Atlassian AdministrationApps[site]Connected apps
  2. Find the app → View app detailsConnections tab
  3. Click Connect under the connector
  4. Fill in any configuration fields (if
    formConfiguration
    was defined)
  5. Click Connect — this triggers
    onConnectionChange
    with
    action: CREATED
    and starts data ingestion
部署完成后,告知用户:
  1. 进入Atlassian管理后台应用[站点]已连接的应用
  2. 找到该应用 → 查看应用详情连接标签页
  3. 点击连接器下方的连接
  4. 填写任何配置字段(如果定义了
    formConfiguration
  5. 点击连接 — 这会触发
    onConnectionChange
    并传入
    action: CREATED
    ,同时开始数据导入

Step 6: Monitor with forge tunnel

步骤6:使用forge tunnel监控

Use
forge tunnel
during development to stream live logs directly to your terminal as the connector functions execute. This is the fastest way to catch errors in
onConnectionChangeHandler
,
validateConnectionHandler
, and
setObjects
calls without waiting for
forge logs
.
Tell the user to run this in their own terminal (it requires an interactive session):
bash
cd <app-directory>
forge tunnel
With the tunnel active, any invocation of the connector functions (e.g. clicking "Connect" in Atlassian Admin, or triggering a scheduled re-ingestion) will stream output immediately. Look for:
  • [connector] Fetched N items
    — confirms
    fetchExternalData()
    ran
  • [connector] Batch 1: N accepted, 0 rejected
    — confirms
    setObjects
    succeeded
  • Any uncaught errors or thrown exceptions from
    validateConnectionHandler
If the tunnel is not running, use
forge logs
instead to inspect past invocations:
bash
undefined
在开发过程中使用
forge tunnel
,将连接器函数执行时的实时日志直接流式传输到你的终端。这是捕获
onConnectionChangeHandler
validateConnectionHandler
setObjects
调用错误最快的方式,无需等待
forge logs
告知用户在自己的终端中运行(需要交互式会话):
bash
cd <应用目录>
forge tunnel
隧道激活后,连接器函数的任何调用(例如在Atlassian管理后台点击“连接”,或触发定时重新导入)都会立即输出内容。注意查看:
  • [connector] Fetched N items
    — 确认
    fetchExternalData()
    已运行
  • [connector] Batch 1: N accepted, 0 rejected
    — 确认
    setObjects
    执行成功
  • validateConnectionHandler
    抛出的任何未捕获错误或异常
如果未运行隧道,可使用
forge logs
查看过往调用记录:
bash
undefined

Most recent 50 log lines from development environment

开发环境最近50条日志

forge logs -e development --limit 50
forge logs -e development --limit 50

Production logs for a specific site

特定站点的生产环境日志

forge logs -e production --site <your-site> --limit 50

**Tunnel vs logs — when to use which:**

| Situation | Use |
|---|---|
| Actively developing / testing the connection flow | `forge tunnel` — live streaming |
| Debugging a past invocation or production issue | `forge logs` |
| Connector function timed out before tunnel caught it | `forge logs` with `--limit 100` |

> **Note:** `forge tunnel` must be run by the user in an interactive terminal — do not attempt to run it via the agent.

---
forge logs -e production --site <你的站点> --limit 50

**隧道vs日志 — 适用场景:**

| 场景 | 使用方式 |
|---|---|
| 积极开发/测试连接流程 | `forge tunnel` — 实时流式传输 |
| 调试过往调用或生产环境问题 | `forge logs` |
| 连接器函数超时,隧道未捕获到 | 使用`forge logs --limit 100` |

> **注意:** `forge tunnel`必须由用户在交互式终端中运行 — 不要尝试通过Agent运行。

---

Manifest Reference

清单参考

Key rules:
  • Scopes are
    read:object:jira
    ,
    write:object:jira
    ,
    delete:object:jira
    — NOT
    read:graph:teamwork
    /
    write:graph:teamwork
    (those fail
    forge lint
    )
  • function:
    is declared under
    modules:
    , not at the top level
  • Egress uses
    address:
    not a bare string (run
    forge lint --fix
    to auto-correct)
  • formConfiguration
    uses
    form: [{ type: header, properties: [...] }]
    — NOT
    fields:
    or
    beforeYouBegin:
核心规则:
  • 权限范围为
    read:object:jira
    write:object:jira
    delete:object:jira
    — 切勿使用
    read:graph:teamwork
    /
    write:graph:teamwork
    (这些会导致
    forge lint
    失败)
  • function:
    声明在**
    modules:
    下**,而非顶层
  • 出口规则使用
    address:
    而非裸字符串(运行
    forge lint --fix
    自动修正)
  • formConfiguration
    使用
    form: [{ type: header, properties: [...] }]
    — 切勿使用
    fields:
    beforeYouBegin:

Minimal connector (no admin config, no OAuth)

最小化连接器(无管理员配置,无OAuth)

Use when the app operates entirely within Atlassian — no external credentials needed.
yaml
app:
  id: <generated-by-forge-create>
  runtime:
    name: nodejs24.x
    memoryMB: 256
    architecture: arm64

permissions:
  scopes:
    - read:object:jira
    - write:object:jira
    - delete:object:jira
    - storage:app

modules:
  graph:connector:
    - key: my-connector
      name: My Service
      icons:
        light: https://cdn.example.com/logo.png
        dark: https://cdn.example.com/logo.png
      objectTypes:
        - atlassian:document
      datasource:
        onConnectionChange:
          function: on-connection-change

  function:
    - key: on-connection-change
      handler: index.onConnectionChangeHandler
适用于完全在Atlassian内部运行的应用 — 无需外部凭据。
yaml
app:
  id: <由forge-create生成>
  runtime:
    name: nodejs24.x
    memoryMB: 256
    architecture: arm64

permissions:
  scopes:
    - read:object:jira
    - write:object:jira
    - delete:object:jira
    - storage:app

modules:
  graph:connector:
    - key: my-connector
      name: My Service
      icons:
        light: https://cdn.example.com/logo.png
        dark: https://cdn.example.com/logo.png
      objectTypes:
        - atlassian:document
      datasource:
        onConnectionChange:
          function: on-connection-change

  function:
    - key: on-connection-change
      handler: index.onConnectionChangeHandler

Connector with admin form config (API key / URL)

带管理员表单配置的连接器(API密钥/URL)

Use when the admin must provide credentials to connect to an external system.
yaml
app:
  id: <generated-by-forge-create>
  runtime:
    name: nodejs24.x
    memoryMB: 256
    architecture: arm64

permissions:
  scopes:
    - read:object:jira
    - write:object:jira
    - delete:object:jira
    - storage:app
  external:
    fetch:
      backend:
        - address: 'https://api.your-service.com'   # note: address: not a bare string

modules:
  graph:connector:
    - key: my-connector
      name: My Service
      icons:
        light: https://cdn.example.com/logo.png
        dark: https://cdn.example.com/logo.png
      objectTypes:
        - atlassian:document
      datasource:
        formConfiguration:
          form:                          # use form:, NOT fields: or beforeYouBegin:
            - key: connectionDetails
              type: header
              title: Connection Details
              description: >
                Provide your My Service API credentials.
                Find them in My Service → Settings → API.
              properties:
                - key: apiKey           # camelCase keys — accessed as request.configProperties.apiKey
                  label: API Key
                  type: string
                  isRequired: true
                - key: apiUrl
                  label: API URL
                  type: string
                  isRequired: true
          validateConnection:
            function: validate-connection
        onConnectionChange:
          function: on-connection-change

  function:                              # function: is under modules:, NOT top-level
    - key: on-connection-change
      handler: index.onConnectionChangeHandler
    - key: validate-connection
      handler: index.validateConnectionHandler

适用于管理员必须提供凭据以连接到外部系统的场景。
yaml
app:
  id: <由forge-create生成>
  runtime:
    name: nodejs24.x
    memoryMB: 256
    architecture: arm64

permissions:
  scopes:
    - read:object:jira
    - write:object:jira
    - delete:object:jira
    - storage:app
  external:
    fetch:
      backend:
        - address: 'https://api.your-service.com'   # 注意:使用address:而非裸字符串

modules:
  graph:connector:
    - key: my-connector
      name: My Service
      icons:
        light: https://cdn.example.com/logo.png
        dark: https://cdn.example.com/logo.png
      objectTypes:
        - atlassian:document
      datasource:
        formConfiguration:
          form:                          # 使用form:,切勿使用fields:或beforeYouBegin:
            - key: connectionDetails
              type: header
              title: 连接详情
              description: >
                提供你的My Service API凭据。
                在My Service → 设置 → API中查找。
              properties:
                - key: apiKey           # 驼峰式命名键 — 通过request.configProperties.apiKey访问
                  label: API密钥
                  type: string
                  isRequired: true
                - key: apiUrl
                  label: API URL
                  type: string
                  isRequired: true
          validateConnection:
            function: validate-connection
        onConnectionChange:
          function: on-connection-change

  function:                              # function:位于modules:下,而非顶层
    - key: on-connection-change
      handler: index.onConnectionChangeHandler
    - key: validate-connection
      handler: index.validateConnectionHandler

Handler Signatures

处理器签名

Critical: Forge passes the request directly as the first argument — it is NOT wrapped under
event.payload
. Config form values are at
request.configProperties
, not
event.payload.config
. Getting this wrong causes
TypeError: Cannot destructure property of undefined
.
关键提示: Forge将请求直接作为第一个参数传递 — 不会包装在
event.payload
下。表单配置值位于
request.configProperties
,而非
event.payload.config
。此处错误会导致
TypeError: Cannot destructure property of undefined

onConnectionChange

onConnectionChange

javascript
const { kvs } = require('@forge/kvs');
const { graph } = require('@forge/teamwork-graph');

exports.onConnectionChangeHandler = async (request) => {
  // request.action, request.connectionId, request.configProperties
  const { action, connectionId, configProperties } = request;

  if (action === 'DELETED') {
    // Atlassian removes Teamwork Graph data automatically on disconnect.
    // Only clean up locally stored credentials.
    await kvs.deleteSecret(connectionId);
    return { success: true };
  }

  // CREATED or UPDATED — persist credentials and ingest data
  await kvs.setSecret(connectionId, configProperties);
  await ingestAllData(connectionId, configProperties);
  return { success: true };
};
javascript
const { kvs } = require('@forge/kvs');
const { graph } = require('@forge/teamwork-graph');

exports.onConnectionChangeHandler = async (request) => {
  // request.action, request.connectionId, request.configProperties
  const { action, connectionId, configProperties } = request;

  if (action === 'DELETED') {
    // Atlassian会在断开连接时自动移除Teamwork Graph数据。
    // 只需清理本地存储的凭据。
    await kvs.deleteSecret(connectionId);
    return { success: true };
  }

  // CREATED或UPDATED — 保存凭据并导入数据
  await kvs.setSecret(connectionId, configProperties);
  await ingestAllData(connectionId, configProperties);
  return { success: true };
};

validateConnection

validateConnection

javascript
const { fetch } = require('@forge/api');

exports.validateConnectionHandler = async (request) => {
  // request.configProperties — NOT event.payload.config
  const { configProperties } = request;

  // Return { success: false, message } to reject — do NOT throw an Error.
  // Return { success: true } to accept.
  const response = await fetch(`${configProperties['apiUrl']}/health`);
  if (!response.ok) {
    return { success: false, message: 'Invalid API credentials. Please check your settings.' };
  }
  return { success: true, message: 'Connection validated successfully.' };
};
javascript
const { fetch } = require('@forge/api');

exports.validateConnectionHandler = async (request) => {
  // request.configProperties — 而非event.payload.config
  const { configProperties } = request;

  // 返回{ success: false, message }表示拒绝 — 切勿抛出Error。
  // 返回{ success: true }表示接受。
  const response = await fetch(`${configProperties['apiUrl']}/health`);
  if (!response.ok) {
    return { success: false, message: '无效的API凭据,请检查你的设置。' };
  }
  return { success: true, message: '连接验证成功。' };
};

refreshIngestion (scheduled trigger)

refreshIngestion(定时触发器)

javascript
exports.refreshIngestionHandler = async () => {
  const activeConnections = await kvs.get('active-connections') ?? [];
  for (const connectionId of activeConnections) {
    const config = await kvs.getSecret(connectionId);
    if (config) await ingestAllData(connectionId, config);
  }
};

javascript
exports.refreshIngestionHandler = async () => {
  const activeConnections = await kvs.get('active-connections') ?? [];
  for (const connectionId of activeConnections) {
    const config = await kvs.getSecret(connectionId);
    if (config) await ingestAllData(connectionId, config);
  }
};

Object Types

对象类型

Objects in bold are indexed in Rovo Search and Rovo Chat.
Object TypeIndexed in RovoBest for
atlassian:document
Files, pages, wiki articles, reports
atlassian:message
Chat messages, emails, comments
atlassian:work-item
Tasks, tickets, issues
atlassian:project
Projects, workspaces
atlassian:space
Team spaces, org units
atlassian:design
Design files (Figma, etc.)
atlassian:repository
Code repositories
atlassian:pull-request
PRs, merge requests
atlassian:commit
Git commits
atlassian:branch
Git branches
atlassian:conversation
Threads, channels
atlassian:video
Video recordings
atlassian:calendar-event
Meetings, events
atlassian:comment
Review comments
atlassian:customer-organization
Customer accounts, orgs
atlassian:build
CI/CD builds
atlassian:deployment
Deployments
atlassian:test
Test cases

加粗的对象会在Rovo Search和Rovo Chat中建立索引。
对象类型是否在Rovo中建立索引适用场景
atlassian:document
文件、页面、维基文章、报告
atlassian:message
聊天消息、邮件、评论
atlassian:work-item
任务、工单、问题
atlassian:project
项目、工作区
atlassian:space
团队空间、组织单元
atlassian:design
设计文件(如Figma)
atlassian:repository
代码仓库
atlassian:pull-request
拉取请求、合并请求
atlassian:commit
Git提交
atlassian:branch
Git分支
atlassian:conversation
线程、频道
atlassian:video
视频录制
atlassian:calendar-event
会议、活动
atlassian:comment
评审评论
atlassian:customer-organization
客户账户、组织
atlassian:build
CI/CD构建
atlassian:deployment
部署
atlassian:test
测试用例

Rovo Search / Rovo Chat Surfacing

Rovo Search / Rovo Chat展示

Once ingested:
  • Objects appear in Rovo Search under a subfilter named after the connector's nickname (set by admin at connection time)
  • Rovo Chat can reference and cite connector objects in responses when queried about topics related to the ingested content
  • Data is not available immediately — allow a few minutes for indexing after
    onConnectionChange
    fires
To verify ingestion is working:
  1. Open Rovo Search on the Jira site
  2. Search for text that appears in an ingested object's
    name
    or
    properties
  3. Filter by the connector nickname to narrow results

导入完成后:
  • 对象会出现在Rovo Search中,位于以连接器昵称(管理员连接时设置)命名的子筛选器下
  • 当查询与导入内容相关的主题时,Rovo Chat可以引用并引用连接器对象
  • 数据不会立即可用 —
    onConnectionChange
    触发后,需等待几分钟完成索引
验证导入是否成功:
  1. 在Jira站点打开Rovo Search
  2. 搜索出现在导入对象的
    name
    properties
    中的文本
  3. 通过连接器昵称筛选结果

Batching Pattern for Large Datasets

大型数据集的分批处理模式

javascript
const { graph } = require('@forge/teamwork-graph');

const BATCH_SIZE = 100;

async function ingestAllData(connectionId, config) {
  const items = await fetchExternalData(config);

  for (let i = 0; i < items.length; i += BATCH_SIZE) {
    const batch = items.slice(i, i + BATCH_SIZE);
    const result = await graph.setObjects({
      connectionId,          // required in every call
      objects: batch.map(item => ({
        schemaVersion: '1.0',
        id: item.id,                      // unique per connectionId
        updateSequenceNumber: 1,
        displayName: item.title,
        url: item.url,
        createdAt: item.createdAt,
        lastUpdatedAt: item.updatedAt,
        permissions: [{
          accessControls: [{ principals: [{ type: 'EVERYONE' }] }],
        }],
        'atlassian:document': {
          type: { category: 'DOCUMENT', mimeType: item.mimeType },
          content: { mimeType: item.mimeType, text: item.title },
        },
      })),
    });
    if (!result.success) {
      console.error(`[connector] setObjects error in batch ${Math.floor(i / BATCH_SIZE) + 1}:`, result.error);
    }
  }
}

javascript
const { graph } = require('@forge/teamwork-graph');

const BATCH_SIZE = 100;

async function ingestAllData(connectionId, config) {
  const items = await fetchExternalData(config);

  for (let i = 0; i < items.length; i += BATCH_SIZE) {
    const batch = items.slice(i, i + BATCH_SIZE);
    const result = await graph.setObjects({
      connectionId,          // 每次调用都必填
      objects: batch.map(item => ({
        schemaVersion: '1.0',
        id: item.id,                      // 每个connectionId下唯一
        updateSequenceNumber: 1,
        displayName: item.title,
        url: item.url,
        createdAt: item.createdAt,
        lastUpdatedAt: item.updatedAt,
        permissions: [{
          accessControls: [{ principals: [{ type: 'EVERYONE' }] }],
        }],
        'atlassian:document': {
          type: { category: 'DOCUMENT', mimeType: item.mimeType },
          content: { mimeType: item.mimeType, text: item.title },
        },
      })),
    });
    if (!result.success) {
      console.error(`[connector] 第${Math.floor(i / BATCH_SIZE) + 1}批setObjects错误:`, result.error);
    }
  }
}

Scheduled Re-Ingestion (optional)

定时重新导入(可选)

To keep data fresh, add a scheduled trigger that re-runs ingestion periodically:
yaml
undefined
为保持数据新鲜,添加定时触发器定期重新运行导入:
yaml
undefined

In manifest.yml — under modules:

在manifest.yml中 — 位于modules:下

scheduledTrigger:
  • key: refresh-trigger function: refresh-ingestion interval: day # prefer 'day' or 'hour'; avoid 'fiveMinutes'
scheduledTrigger:
  • key: refresh-trigger function: refresh-ingestion interval: day # 优先使用'day'或'hour';避免使用'fiveMinutes'

Under function:

在function:下:

  • key: refresh-ingestion handler: index.refreshIngestionHandler

```javascript
const { kvs } = require('@forge/kvs');

// Track active connections in onConnectionChangeHandler:
//   await kvs.set('active-connections', [...activeConnections, connectionId]);
//   await kvs.setSecret(connectionId, configProperties);  // store credentials securely

exports.refreshIngestionHandler = async () => {
  const activeConnections = await kvs.get('active-connections') ?? [];
  for (const connectionId of activeConnections) {
    const config = await kvs.getSecret(connectionId);  // retrieve stored credentials
    if (config) await ingestAllData(connectionId, config);
  }
};

  • key: refresh-ingestion handler: index.refreshIngestionHandler

```javascript
const { kvs } = require('@forge/kvs');

// 在onConnectionChangeHandler中跟踪活跃连接:
//   await kvs.set('active-connections', [...activeConnections, connectionId]);
//   await kvs.setSecret(connectionId, configProperties);  // 安全存储凭据

exports.refreshIngestionHandler = async () => {
  const activeConnections = await kvs.get('active-connections') ?? [];
  for (const connectionId of activeConnections) {
    const config = await kvs.getSecret(connectionId);  // 检索存储的凭据
    if (config) await ingestAllData(connectionId, config);
  }
};

Scripts

脚本

ScriptSkill directoryPurpose
scripts/scaffold_connector.py
skills/forge-connector/
(this skill)
Scaffold a new connector app — generates manifest.yml, src/index.ts, installs SDK. Run:
python3 -m scripts.scaffold_connector
scripts/deploy_forge_app.py
skills/forge-app-builder/
(different skill)
Deploy and install on Jira. Run from the forge-app-builder directory:
python3 -m scripts.deploy_forge_app
The scaffold script is in this skill's directory. The deploy script is in the forge-app-builder skill directory — always
cd
there (or derive the path from this SKILL.md's location) before running it.

脚本技能目录用途
scripts/scaffold_connector.py
skills/forge-connector/
(本技能)
搭建新的连接器应用 — 生成manifest.yml、src/index.ts、安装SDK。运行方式:
python3 -m scripts.scaffold_connector
scripts/deploy_forge_app.py
skills/forge-app-builder/
其他技能
在Jira上部署和安装。从forge-app-builder目录运行:
python3 -m scripts.deploy_forge_app
脚手架脚本位于本技能的目录中。部署脚本位于forge-app-builder技能目录中 — 运行前务必
cd
到该目录(或从本SKILL.md的位置推导路径)。

Troubleshooting

故障排除

ProblemAction
graph:connector
not recognized in manifest
Run
forge lint
— it will identify the exact field causing the error
TypeError: Cannot destructure property 'config' of 'event.payload'
Handler using
event.payload.config
— change to
request.configProperties
. Forge passes request directly, not nested under
event.payload
TypeError: Cannot read properties of undefined (reading 'set')
Using
storage
from
@forge/storage
— switch to
kvs
from
@forge/kvs
graph.setObjects is not a function
Wrong import — use
const { graph } = require('@forge/teamwork-graph')
then call
graph.setObjects({ objects, connectionId })
forge lint
: invalid scopes
read/write:graph:teamwork
Replace with
read:object:jira
,
write:object:jira
,
delete:object:jira
forge lint
:
document should NOT have additional property 'function'
function:
is at the top level — move it inside
modules:
forge lint
:
formConfiguration must have required property 'form'
Replace
fields:
/
beforeYouBegin:
with
form: [{ type: header, properties: [...] }]
forge lint
warning: deprecated egress entries
Run
forge lint --fix
to auto-convert bare URL strings to
{ address: 'url' }
forge developer-spaces list
command not found
Does not exist in Forge CLI 12.x. Have user run
forge create
interactively to select a developer space
forge create
fails with non-TTY error
forge create
needs an interactive terminal — ask the user to run it; then write manifest and source files into the created directory
onConnectionChange
not triggered
Verify admin clicked "Connect" in Atlassian Administration → Connected apps; run
forge tunnel
to confirm the function fires
Objects not appearing in Rovo SearchWait ~5 minutes for indexing; run
forge logs -e development --since 15m
to check for
setObjects
errors
403 on
@forge/teamwork-graph
calls
Ensure
read:object:jira
,
write:object:jira
,
delete:object:jira
are in manifest scopes, then redeploy and
forge install --upgrade
forge login
required
Create API token at https://id.atlassian.com/manage/api-tokens, then run
forge login


问题解决方法
清单中
graph:connector
不被识别
运行
forge lint
— 它会识别导致错误的具体字段
TypeError: Cannot destructure property 'config' of 'event.payload'
处理器使用了
event.payload.config
— 改为
request.configProperties
。Forge直接传递请求,而非嵌套在
event.payload
TypeError: Cannot read properties of undefined (reading 'set')
使用了
@forge/storage
中的
storage
— 切换为
@forge/kvs
中的
kvs
graph.setObjects is not a function
导入方式错误 — 使用
const { graph } = require('@forge/teamwork-graph')
,然后调用
graph.setObjects({ objects, connectionId })
forge lint
: 无效权限范围
read/write:graph:teamwork
替换为
read:object:jira
write:object:jira
delete:object:jira
forge lint
:
document should NOT have additional property 'function'
function:
位于顶层 — 将其移到
modules:
内部
forge lint
:
formConfiguration must have required property 'form'
fields:
/
beforeYouBegin:
替换为
form: [{ type: header, properties: [...] }]
forge lint
警告:已弃用的出口条目
运行
forge lint --fix
自动将裸URL字符串转换为
{ address: 'url' }
forge developer-spaces list
命令不存在
Forge CLI 12.x中无此命令。让用户交互式运行
forge create
以选择开发者空间
forge create
因非TTY错误失败
forge create
需要交互式终端 — 让用户自行运行;然后将清单和源文件写入创建的目录
onConnectionChange
未触发
确认管理员已在Atlassian管理后台 → 已连接的应用中点击“连接”;运行
forge tunnel
确认函数已触发
对象未出现在Rovo Search中等待约5分钟完成索引;运行
forge logs -e development --since 15m
检查
setObjects
错误
@forge/teamwork-graph
调用返回403
确保清单权限范围包含
read:object:jira
write:object:jira
delete:object:jira
,然后重新部署并运行
forge install --upgrade
需要
forge login
https://id.atlassian.com/manage/api-tokens创建API令牌,然后运行`forge login`


Naming and Logo Guidelines

命名与图标指南

  • Use the official service name as the connector name (e.g.
    Google Drive
    , not
    Drive Connector by Acme
    )
  • Use the official service logo for icons — do not modify or combine with your own branding
  • These guidelines apply only to the
    graph:connector
    module; your Forge app itself may use your own branding
  • 使用官方服务名称作为连接器名称(例如
    Google Drive
    ,而非
    Drive Connector by Acme
  • 使用官方服务图标作为应用图标 — 不要修改或与自有品牌组合
  • 这些指南仅适用于
    graph:connector
    模块;你的Forge应用本身可以使用自有品牌