LINE connects to Apus via the LINE Messaging API. The plugin runs as a webhook receiver on the Gateway and uses your channel access token + channel secret for authentication.

Status: official plugin, installed separately. Direct messages, group chats, media, locations, Flex messages, template messages, and quick replies are supported. Reactions and threads are not supported.

Install

Install LINE before configuring the channel:

apus plugins install @openclaw/line

Local checkout (when running from a git repo):

apus plugins install ./path/to/local/line-plugin

Setup

  1. Create a LINE Developers account and open the Console: https://developers.line.biz/console/
  2. Create (or pick) a Provider and add a Messaging API channel.
  3. Copy the Channel access token and Channel secret from the channel settings.
  4. Enable Use webhook in the Messaging API settings.
  5. Set the webhook URL to your Gateway endpoint (HTTPS required):
https://gateway-host/line/webhook

The Gateway answers LINE's signed webhook verification request: a POST with an empty events list. Signed events in LINE's standby mode are acknowledged without queueing or replying, because another channel holds chat control. Other signed inbound events enter the durable ingress queue before 200. Agent processing continues asynchronously. Failed delivery is retried from the queue, including after a Gateway restart, and poison events become failed queue records after bounded retries. If durable persistence fails, the request returns 500 instead of acknowledging an event that could be lost. Delivery is at least once across the queue-to-agent boundary: a Gateway shutdown or crash during an active delivery can replay the turn. Message events deduplicate by LINE message ID. Other event types use webhookEventId. Retained completion records suppress ordinary duplicate webhooks, but handlers that perform external side effects should still be idempotent. If you need a custom path, set channels.line.webhookPath or channels.line.accounts.<id>.webhookPath and update the URL accordingly.

Security notes:

Inbound durability

The Setup webhook contract acknowledges an event only after it is durably queued. The durable 200 carries x-apus-delivery-accepted: durable. Signed verification pings (empty event lists), standby-only batches, and error responses omit the marker, so reverse proxies can require it to distinguish durable acceptance from a generic 200. From there, delivery runs through the core channel-ingress drain with LINE-specific settings:

The 500-on-persistence-failure contract only helps if LINE re-sends the event. LINE redelivers a webhook when Webhook redelivery is enabled for the channel in the LINE Developers Console (Messaging API settings, alongside Use webhook) and the bot server did not answer 2xx. Without that setting, an event refused with 500 is not re-sent. Even enabled, redelivery is best effort rather than a guarantee: LINE documents that it is not reliable, that the retry count and interval are undisclosed, and that redelivered events may arrive out of order or more than once (the duplicate suppression window above absorbs the repeats). See Redeliver a webhook that failed to be received.

Dead-lettered events stay inspectable and, depending on the failure reason, recoverable. See Inbound dead letters and Troubleshooting below.

Configure

Minimal config:

{
  channels: {
    line: {
      enabled: true,
      channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
      channelSecret: "LINE_CHANNEL_SECRET",
      dmPolicy: "pairing",
    },
  },
}

Public DM config:

{
  channels: {
    line: {
      enabled: true,
      channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
      channelSecret: "LINE_CHANNEL_SECRET",
      dmPolicy: "open",
      allowFrom: ["*"],
    },
  },
}

Env vars (default account only):

Token/secret files:

{
  channels: {
    line: {
      tokenFile: "/path/to/line-token.txt",
      secretFile: "/path/to/line-secret.txt",
    },
  },
}

tokenFile and secretFile must point to regular files. Symlinks are rejected. Inline config values win over files. Env vars are the last fallback for the default account.

Multiple accounts:

{
  channels: {
    line: {
      accounts: {
        marketing: {
          channelAccessToken: "...",
          channelSecret: "...",
          webhookPath: "/line/marketing",
        },
      },
    },
  },
}

Access control

Direct messages default to pairing. Unknown senders get a pairing code and their messages are ignored until approved:

apus pairing list line
apus pairing approve line <CODE>

Allowlists and policies:

LINE IDs are case-sensitive. Valid IDs look like:

Directory

apus directory peers list --channel line lists user IDs from the selected account's allowFrom, groupAllowFrom, and per-group allowFrom entries. apus directory groups list --channel line lists configured group and room IDs. Prefixes normalize to sendable IDs, duplicates appear once, and * and accessGroup:<name> entries are omitted. Use --account, --query, --limit, and --json as described in Directory.

These lists read configuration. They do not fetch a live LINE contact roster or include approvals stored through pairing.

Group join introductions

When the bot joins an allowed group or multi-person room, it posts one introduction there. LINE exposes a group name through its group summary API, but no room name or topic for multi-person rooms. The Messaging API cannot read prior messages, so introductions use only available metadata and ask what the room wants the bot to take on rather than inventing activity.

Introductions are enabled by default. Set channels.line.joinIntro: false to disable them, or use channels.line.accounts.<accountId>.joinIntro to override one account. They never run in one-to-one user chats or when another member joins. See group join introductions for room admission, once-per-room behavior, and the no-tools turn that treats room content as untrusted.

Message behavior

Reply quoting

channels.line.replyToMode controls native quote replies (an outbound reply visibly quotes the message it answers, which is how a group tells who the bot is talking to):

Value Behavior
"off" (default) Do not quote automatically
"first" Quote only the first reply of a turn
"all" Quote every reply of a turn

"batched", which other channels accept, is rejected here: it distinguishes a reply to a coalesced turn from a reply to an immediate one, and nothing on the LINE path marks a turn as coalesced, so that distinction never arises.

Per-account override: channels.line.accounts.<id>.replyToMode. There is no per-chat-type override: replyToModeByChatType, which Slack, Signal, and Mattermost accept, is rejected here, so one account quotes the same way in direct chats and groups.

{ channels: { line: { replyToMode: "all" } } }

As on Telegram, "off" turns off automatic quoting only: an explicit reply tag the agent writes is still honoured. Replies quote inline and stay visible in the conversation, so nothing is hidden by threading them.

LINE quotes by a token it issues with each inbound message rather than by message id, and Apus can only quote a message it kept that token for. Quoting therefore has limits the setting cannot lift:

Block streaming

Block streaming sends each completed assistant block as its own LINE message instead of waiting for the whole reply. It is off by default, and channels.line.streaming decides it for LINE alone:

{ channels: { line: { streaming: { block: { enabled: true } } } } }
Setting Effect
streaming.block.enabled: true Send completed blocks as they finish, whatever the agent default is
streaming.block.enabled: false Keep LINE on whole replies even when the agent default is on
unset (default) Follow agents.defaults.blockStreamingDefault
streaming.block.coalesce Merge small blocks before sending (minChars, maxChars, idleMs)
streaming.chunkMode length (default) or newline to split on paragraph boundaries

Per-account override: channels.line.accounts.<id>.streaming. These are the block-mode streaming controls shared across channels; see Streaming.

Each block LINE receives is a separate message, and LINE counts messages against the channel's monthly quota, so leaving this off keeps a long reply to the fewest messages. coalesce.minChars is the lever if you want blocks to arrive early but not one paragraph at a time — Apus's own default is 800 characters.

LINE cannot edit a message it has already sent, so it has no preview streaming mode: there is no streaming.mode or streaming.preview here, and a reply is never revised in place.

Structured rich messages

Use the shared message presentation fields for portable choices. LINE renders buttons blocks as Flex controls and select blocks as quick replies. A two-button block is the portable confirm-style form.

A buttons block renders a Flex card that carries the presentation's title and text. A presentation whose only control is a select renders no card, because quick replies attach to the reply's own text message. Its title and text blocks are appended to that text instead. LINE draws at most 13 quick replies on one message, counted across every select block in the reply rather than per block. Each select keeps its prompt and any overflow options together in that text. Prompts and overflow option names remain complete. Only native quick-reply button labels are shortened to LINE's 20-character limit.

In direct chats, the options an ask_user question offers become tappable controls on the same Flex card, and a tap answers the question directly. LINE carries the option index the Gateway assigned rather than the label, so a reply whose choices the Gateway no longer lists falls back to readable text instead of drawing a tap that answers the wrong option. The eligible shape is one single-select, non-secret question offering two to four distinct options — the same bound Telegram, Discord and Slack use; anything else stays readable text that a typed reply still answers. Groups, multi-person chats, and unrecognized destinations also use this readable fallback. LINE's group and room postbacks do not include the sender identity needed to admit a question answer; reply with the option text instead.

The Other… free-text control is not drawn. Tapping it resolves nothing by itself, and LINE cannot take a control back off a card it already delivered, so the button would add a tap that changes nothing the question's own text does not already offer. Discord and Slack leave that route in text for the same reason. In eligible direct chats, each declared option keeps a native control, and Other… stays named in the card's text under Actions: whatever the option count.

LINE cannot edit a message it already delivered, so the controls stay on screen after the question ends. A tap that arrives then is answered with That question is no longer waiting for an answer. Initial taps follow the channel's normal admission and pairing rules. If pairing is revoked while the question is being read, the answer is ignored without an answer notice or a new pairing challenge. The Gateway reports one terminal state for answered, cancelled and expired questions alike, so the notice does not claim which one it was.

{
  action: "send",
  message: "Choose an action",
  presentation: {
    title: "Menu",
    blocks: [
      {
        type: "buttons",
        buttons: [
          { label: "Status", action: { type: "command", command: "/status" } },
          { label: "Website", action: { type: "url", url: "https://example.com" } },
        ],
      },
      {
        type: "select",
        placeholder: "Pick one",
        options: [
          { label: "Alpha", action: { type: "callback", value: "alpha" } },
          { label: "Help", action: { type: "command", command: "/help" } },
        ],
      },
    ],
  },
}

LINE-only output uses the schema-validated channelData.line fields on message(action="send"). Send one location and/or one card. The supported card types are media_player, event, agenda, device, and appletv_remote.

{
  action: "send",
  message: "Here you go",
  channelData: {
    line: {
      location: {
        title: "Office",
        address: "123 Main St",
        latitude: 35.681236,
        longitude: 139.767125,
      },
      card: {
        type: "event",
        title: "Team meeting",
        date: "2026-08-18",
        time: "10:00",
        location: "Conference room",
        description: "Weekly planning",
      },
    },
  },
}

Other card shapes:

{ type: "media_player", title: "Song", artist: "Artist", source: "Living Room", status: "playing", imageUrl: "https://example.com/cover.jpg" }
{ type: "agenda", title: "Today", events: [{ title: "Standup", time: "09:00", location: "Online" }] }
{ type: "device", name: "TV", deviceType: "Streaming box", status: "Playing", controls: [{ label: "Pause", action: "pause" }] }
{ type: "appletv_remote", name: "Living Room", status: "Playing" }

Double-bracket strings such as [[buttons: ...]] are plain text and are not interpreted as rich-message instructions.

The LINE plugin also ships a /card command for Flex message presets:

/card info "Welcome" "Thanks for joining!"

Card images and icons must use HTTPS. Apus removes images with malformed or non-HTTPS URLs and adds an "Image unavailable" note when it fits within LINE's 30 KB bubble and 50 KB carousel limits. Video heroes keep their required alternative content: an unusable video or preview URL falls back to that content, and an unusable alternative image becomes a text box. Invalid template thumbnails are removed. Carousel thumbnails are removed together so every column keeps the same image layout. Text and action buttons stay intact.

ACP support

LINE supports ACP (Agent Communication Protocol) conversation bindings:

See ACP agents for details.

Outbound media

The LINE plugin sends images, videos, and audio through the agent message tool:

When mediaKind is omitted, LINE infers it from LINE-specific options or the URL suffix. Native suffix inference supports JPEG/PNG, MP4, and MP3/M4A. Suffixless URLs retain the image fallback. Other suffixed URLs and inferred MP4 without a preview become text links. Explicit video still requires previewImageUrl.

Outbound media URLs must be public HTTPS URLs of at most 2000 characters. Apus validates the target hostname before handing the URL to LINE and rejects loopback, link-local, and private-network targets.

Troubleshooting

Related