# Lark guide

> Bring your own Lark (open.larksuite.com) or Feishu (open.feishu.cn) custom or App Store app. Configure its event URL and connect a direct chat or group. App Store bots also require an enabled installation in that organization.

## Funding and paused tasks

Every workspace, including one with a single member, has its own funding context.
If a channel request cannot run for lack of credit, the bot reports it here and
provides that workspace’s Billing link. A task budget or member spending limit
gets its own recovery instructions. After adding credit, check the conversation. Existing recovery policies remain in
effect; if it is still blocked, send “continue” there to resume the same session.

## Connect your company

Open **Channels → Connect Lark**. An administrator completes setup once; employees
do not link individual AgentSky accounts. The company uses one shared workspace.

Choose **Copy message** and paste it into Codex, Claude Code or another
agent with terminal or browser access. The instructions include this workspace,
required scopes, the current callback when available, and end-to-end verification.
They contain no credentials. The agent should prefer the official SDK's
[device registration](https://github.com/larksuite/node-sdk#app-registration): you
confirm in Lark while it handles app creation. Availability varies; requested
scopes must be verified, and some apps still need console configuration.

For agents doing the setup: use the official Node SDK's `registerApp`
where supported. For Lark international, set `domain` and `larkDomain` to
`accounts.larksuite.com`; for Feishu use `accounts.feishu.cn`. Use
`createOnly: true` for a new app and `addons.preset: false`, with the tenant
scopes listed below and event `im.message.receive_v1`. Reuse an existing app
when present. Addons can be unavailable or ignored: verify the actual scopes.
Show its official verification URL as a QR code plus an **Open Lark** link for
mobile. Keep the SDK registration process running until confirmation, expiry or
cancellation. Never put a device code or app secret in the QR image.

Transfer credentials directly through the signed-in setup form or authenticated
API using a workspace-scoped credential; never paste them in chat or use an
unscoped API token. If secure transfer is unavailable, let the administrator enter
them in the form. Register the app in AgentSky before configuring its returned
callback URL so the challenge can succeed. Eligible custom apps can use
`client.application.v7.applicationConfig.patch` with
`event.subscription_type: webhook`, the returned URL, and
`event.add_events: [im.message.receive_v1]`. Also configure
`callback: { callback_type: "webhook", request_url: "<the same returned URL>", add_callbacks: ["card.action.trigger"] }`.
Preserve unrelated settings.
Device-created apps may not support this API: handle the provider's response
and complete those settings in the console when needed. Publish with administrator
approval, then connect the workspace and verify a real bot reply. Saved credentials
or a successful API response alone do not prove messaging works. Verify group
conversations, attachments and authorized documents when those features are needed.

For manual setup:

1. Create a custom app in your organization's Lark Developer Console and enable Bot.
2. Paste **App ID**, **App Secret** and **Verification Token** into AgentSky, then
   choose **Verify and continue**. Feishu and encryption options are under advanced settings.
3. Copy the permission scopes into Lark's batch-import dialog. Copy the returned
   event URL into Event Subscriptions, add `im.message.receive_v1`, and use the same
   URL under Callback Configuration with `card.action.trigger`. Publish the
   version and make it available to your organization.
4. Choose **Connect workspace**, open your bot and send the connection code once.
   Wait for its connection confirmation before assigning work.

Members can continue a shared session or send `/new` to start another. Send
`/status` for the current task's progress and session link, `/tasks` to see
workspace tasks, or `/tasks N` to continue a listed task here. `/help` explains
the controls and `/stop` interrupts the current task. Native topics have their
own context. Progress, results and files are delivered in Lark;
the session link provides the full process when needed. An ordinary message does
not approve a pending human request; explicit request actions require the verified
administrator who connected the bot.

When native callbacks are enabled, single-choice questions have buttons. A click is acknowledged only after
it is stored for processing; approval still depends on the original request and
the clicking member's authority. Expired or replaced choices are refused. Multiple
choices and unsupported controls remain text replies. If buttons do nothing,
check **Callback Configuration**, enable `card.action.trigger` and publish the
updated app. Event Subscriptions alone does not enable button callbacks.

A custom app belongs to its own Lark organization. Another company configures its
own app; an internal company bot is not a global click-to-authorize install.
App Store installation is described separately below.

## Prerequisites

Use a **custom or App Store app** in the [Lark Developer Console](https://open.larksuite.com/app) (or the Feishu console) with the **Bot** capability enabled. Grant the permissions below; each item explains what stops working without that permission:

- `im:message`, `im:message.p2p_msg:readonly` — the bot receives nothing in a direct chat.
- `im:message.group_at_msg:readonly` — the bot receives nothing when mentioned in a group.
- `im:message.group_msg` — ordinary group follow-ups without a bot mention are not delivered; requires administrator approval.
- `im:message:send_as_bot` — the bot cannot reply.
- `im:message:update` — a streamed reply cannot be revised — it arrives as one block at the end.
- `im:message:recall` — a reply that gets shorter leaves its stale tail behind.
- `im:resource` — native images and other file attachments cannot be read or uploaded.
- `docx:document:readonly` — authorized native Docs links cannot be read. Share the document with the bot first; this scope does not grant access to private documents.
- `im:message.reactions:write_only` or `im:message` — add and remove working markers (OnIt → DONE).
- `im:message.reactions:read` or `im:message:readonly` — recover interrupted removals when the marker’s handle is known. Normal confirmed add/remove works with write permission alone; uncertain operations may require administrator recovery, and an uncertain removal blocks re-adding that same marker even after cleanup.

Automated status reactions belong to their current turn. An explicit reaction request that collides with one returns `409 reaction_owner_conflict`; choose a different emoji. The service removes the previous turn’s status before starting the next one, and leaves explicitly added reactions alone. Finishing a Lark turn requires the matching `marker_ref` from its reply instructions; a reply without that reference still sends, but does not clear the status.

  Permissions take effect when you publish an app version. Request the all-group-messages scope only when you want group conversations to continue without a mention; it permits receiving messages in every group containing the bot. With mention-only permission, mention the bot on every turn. See the [official SDK permission guidance](https://github.com/larksuite/node-sdk/blob/main/docs/channel.md#common-issues).
- Its **App ID** and **App Secret** (Credentials & Basic Info) and the **Verification Token** from Event Subscriptions. If you set an **Encrypt Key** there, pass it too — deliveries then arrive encrypted and signed.
- An AgentSky API token with write scope.

## Register the channel app

The [developer console](https://platform.agentsky.dev/developer/apps/new?platform=lark) walks the same checklist, or over the API:

```bash
curl -s -X POST https://api.agentsky.dev/v1/channels/apps \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"platform": "lark", "label": "My Lark app", "credentials": {"app_id": "cli_...", "app_secret": "...", "verification_token": "...", "domain": "lark"}}'
# -> setup: { "events_url": "https://.../webhooks/lark/app_..." }
```

Custom-app registration mints a tenant token and reads the bot's identity — a wrong secret answers `400 lark_error`. The stored credential keys grow `bot_open_id` and `bot_name`. `domain` is `lark` (default) or `feishu`; `encrypt_key` is optional.

**Configure events.** Eligible custom apps can use the official
[application config API](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/application-v7/application-v7/application-config/patch)
to configure event subscriptions. Check the app's eligibility and the actual
response; device-created apps may require console setup. In the console, paste `setup.events_url` as the **Request URL** under Event Subscriptions (Lark verifies it with a challenge immediately), add the **`im.message.receive_v1`** event, and configure the same URL under **Callback Configuration** with **`card.action.trigger`**. Then publish an app version so the bot and callbacks are available to your organization.

## App Store applications

Choose **App Store app** when the bot is installed in multiple organizations and
set `app_mode: store`. Registration returns the events URL; it does not prove
that an organization has installed the app. Send `app_ticket`, `app_open`,
`app_status_change` and `app_uninstalled` events to that same URL, in addition
to `im.message.receive_v1`. Messaging is refused until the tenant is enabled.
Removing or stopping an installation prevents later sends and queued file reads.

To migrate an existing installation, an administrator may register its current
`app_ticket` and adopt the existing conversation with `adopt.tenant_key`.
The service checks the provider's current application and chat state before
binding that organization. A missing, disabled or mismatched installation is
refused; registration alone is never a completed migration.

## Files

Upload a file through the Files API, then send its `file_id` as a
`{type: "file", file_id}` part on the existing thread-messages endpoint.
The file must be visible in the connection's workspace. Lark receives the original
bytes; incoming file/image resources become scoped Files identifiers in the signed
event. Images are limited to 10 MiB each and other files to 30 MiB each; the combined
message attachment budget is 32 MiB. Queued downloads recheck
the current app credentials, so disabled apps cannot retrieve previously queued
resources. If delivery reports a partial or unknown result, inspect the chat
before sending again with a new idempotency key.

## Connect

```bash
curl -s -X POST https://api.agentsky.dev/v1/channels/connections \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"platform": "lark", "app": "app_...", "destination": {"session": "sess-..."}}'
# -> connect: { "url": "https://applink.larksuite.com/client/bot/open?appId=cli_...", "code": "LINK:…", "app_id": "cli_...", "expires_at": "…" }
```

`connect.url` opens the bot's chat inside Lark; the end user sends `connect.code` there and the chat claims the connection. To connect a **group**, add the bot to the group and send the code in the group with an @mention of the bot. The connection's thread is then `lark:{app}:GROUP:{chat_id}` and the service subscribes that conversation after the claim. With administrator-approved `im:message.group_msg` permission, subsequent messages need no mention; with mention-only permission, every follow-up must mention the bot. Unconnected, unmentioned groups are not routed to an agent. Codes expire after 15 minutes, and a code minted for your app is refused if sent to any other bot. The [hosted connect page](https://agentsky.dev/docs/channels/routing.md#the-hosted-connect-flow) renders this ceremony for you.

## Capabilities

| threads | reactions | markers | proactive | markdown | modals | ephemeral | streaming |
|---|---|---|---|---|---|---|---|
| ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |

Markdown renders as an interactive card (Lark's `markdown` element: bold, italic, links, inline code, code blocks, lists, headings), which is also what lets replies be edited in place. A `raw` part is sent as a plain text message. Working markers use Lark's reaction set — **OnIt** while working, **DONE** on completion, **CrossMark** on failure. Thread ids are `lark:P2P:{chat_id}` / `lark:GROUP:{chat_id}` for the main conversation (with the app id as the second segment on your own app). Private replies carry `lark:P2P:{chat_id}:REPLY:{root_message_id}` for a flat reply or `lark:P2P:{chat_id}:TOPIC:{root_message_id}` for a Lark sub-thread; group replies use the corresponding `lark:GROUP:{chat_id}:REPLY:{root_message_id}` or `lark:GROUP:{chat_id}:TOPIC:{root_message_id}`. Use the matching chat-level channel id (`lark:P2P:{chat_id}` or `lark:GROUP:{chat_id}`) to send to the top-level conversation regardless of which reply-scoped thread id delivered the inbound message.

## Troubleshooting

- **The console refuses the Request URL** — the app was registered with a different verification token than the console shows; rotate the app's credentials (`PATCH /v1/channels/apps/{id}`) with the current token and try again.
- **Nothing arrives from a group** — the bot only receives group messages that @mention it unless the app holds the receive-all-group-messages scope; mention the bot, and check the event subscription includes `im.message.receive_v1`.
- **Replies fail with a permission error** — grant `im:message:send_as_bot` (and the reactions scope for markers) and re-publish the app version; permissions take effect on publish.
- **Encrypted deliveries answer 401** — the `encrypt_key` credential does not match the console's Encrypt Key.
