Skip to content

Push Alerts to WeCom (WeChat Work) via RPC

1. Overview

ThinkLink (TKL) RPC can call external HTTP endpoints (see RPC Model §1.3.2 Type 4) and forward device alerts, status changes, or custom events to a WeCom group bot in real time. When a device triggers an alert condition, on-call engineers can be notified immediately in their WeCom group.

This note shows how to push messages to WeCom from an RPC. Three integration paths are covered (in recommended order):

PathWhen to useAuth (storage location)
Group bot (§2–§3)Push to a single fixed WeCom groupWebhook URL (Org Param wecom_webhook_url)
Smart bot (§6)One bot needs to serve multiple groups, or you need template-card / callback featuresWebhook URL (Org Params)
Self-built app (§7)Target specific users / departments / tags (no group required)corpid + corpsecret + agentid (Org Params)

2. Prerequisites

2.1 Create a WeCom Group Bot

  1. In a WeCom group, open Group Settings → Group Bots → Add Bot
  2. Set the bot name and avatar, then copy the generated Webhook URL
  3. The webhook URL looks like:
    https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    The key query parameter is the only thing protecting this bot.

2.2 Protect the Webhook Key (Required Reading)

⚠️ Security notice: The key in the webhook URL is the bot's full send-permission credential. Anyone who has it can post to your group.

Follow these rules:

  • Do not store the key in server_attrs, asset attributes, trigger models, or any other field that callers can read — those fields travel with the device/asset and broaden the leak surface
  • Recommended: store the full webhook URL in System Management → Server Configuration → Org Params, and read it from the RPC as org_params.wecom_webhook_url. Why:
    • The Org Params page is visible to org admins only — regular operators and RPC callers cannot see it
    • Rotating the key is a single edit in Server Configuration; every trigger, scheduled task, and external caller that uses the RPC automatically picks up the new value with no script redeploy
    • Multiple bots in the same org (prod/staging, alarm/daily-report) can live under different keys without interfering with each other
    • See Server Configuration §1.6 Org Params
  • Rotate the webhook key at least once a quarter by clicking "Reset webhook URL" in the bot settings, then update only the Org Param value
  • Limit group membership; outsiders joining the group also gain access to the webhook
  • If you suspect the key has leaked, reset it immediately in the bot settings, update the Org Param value, and review recent group messages for unexpected posts

3. Calling WeCom from an RPC

3.1 Design Principle

Treat the WeCom alert RPC as a "notification service":

  • Secret stays inside: the webhook URL lives in Org Params (org_params.wecom_webhook_url), visible to org admins only
  • Interface faces outward: callers pass message text, alert level, mention targets, etc. via params
  • Multiple bots: register several keys in Org Params (e.g. wecom_webhook_alarm, wecom_webhook_daily) and route in-script via params.channel; the keys never leave Server Configuration

3.2 Prerequisite: Store the Webhook in Org Params

Open System Management → Server Configuration → Org Params and add an entry:

KeyValueRemark
wecom_webhook_urlhttps://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-...WeCom alarm-bot webhook, rotate quarterly

The new value is effective immediately for all RPCs — no service restart required.

3.3 RPC Script: Plain Text Message

javascript
function rpc_script({ device, params, alarms, logger, org_params }) {
    let webhook = org_params?.wecom_webhook_url;
    if (!webhook) {
        logger.error("wecom_webhook_url not configured in org_params");
        return null;
    }
    let content = params.content || `Device ${device.name}(${device.eui}) alert`;

    return [
        {
            sleepTimeMs: 0,
            type: "axios",
            dnMsg: {
                method: "POST",
                url: webhook,
                headers: { "Content-Type": "application/json" },
                data: {
                    msgtype: "text",
                    text: { content: content }
                },
                timeout: 5000
            }
        }
    ];
}

This script accepts the same input params as the built-in ALARM RPC, and is intended to be called by the same trigger model so that a single uplink produces both a platform alarm record and a WeCom group message.

Key behaviors:

  • action = "new": pushes a "Device Alarm Raised" message
  • action = "clear": first checks whether the alarm is currently active via alarms[name]. Only an active alarm being cleared triggers a "Alarm Recovered" push — repeated clear calls while the condition stays normal will not spam the group
  • Params aligned with ALARM RPC: name / action / title / level / desc use identical names and meanings, so the trigger can feed the same params object to both RPCs
  • Webhook sourced from org_params: nothing is hardcoded in the script
javascript
function rpc_script({ device, params, alarms, logger, org_params }) {
    let webhook = org_params?.wecom_webhook_url;
    if (!webhook) {
        logger.error("wecom_webhook_url not configured in org_params");
        return null;
    }

    let name   = params.name;
    let action = params.action;
    let title  = params.title || name || "";
    let level  = params.level || "mid";
    let desc   = params.desc  || "";

    if (action === "clear") {
        // Only push a recovery message if this alarm_name is currently active.
        // Prevents flooding the group with clear messages on every uplink while
        // the condition has been normal for a long time.
        if (!alarms || alarms[name] === undefined) {
            return null;
        }
    } else if (action !== "new") {
        // Unknown action (e.g. "no"): do not push
        return null;
    }

    let levelColor = {
        urgent: "warning",
        high:   "warning",
        mid:    "comment",
        low:    "info"
    }[level] || "info";

    let header = action === "new" ? `### Device Alarm` : `### Alarm Recovered`;

    let content = [
        header,
        `> **Device**: ${device.name}`,
        `> **EUI**: \`${device.eui}\``,
        `> **Alarm Name**: \`${name}\``,
        `> **Title**: ${title}`,
        `> **Level**: <font color="${levelColor}">${level.toUpperCase()}</font>`,
        `> **${action === "new" ? "Description" : "Recovery note"}**: ${desc || "n/a"}`,
        `> **Time**: ${new Date().toISOString()}`
    ].join("\n");

    return [
        {
            sleepTimeMs: 0,
            type: "axios",
            dnMsg: {
                method: "POST",
                url: webhook,
                headers: { "Content-Type": "application/json" },
                data: {
                    msgtype: "markdown",
                    markdown: { content: content }
                },
                timeout: 5000
            }
        }
    ];
}

💡 alarms is one of the RPC inputs — a map keyed by alarm_name of currently active alarms. A key present means the alarm is active; absent (undefined) means it was never raised or has already been cleared. See Alarm Management §1.6.2.

4. RPC Parameter Configuration

Register the following parameter fields on this RPC (intentionally aligned with the ALARM RPC so the trigger script can share one params object):

Attr NameTypeAliasDescription
namestringAlarm event nameUnique key, must match the name passed to the ALARM RPC — used for alarms[name] lookup
actionstringActionnew raises an alarm / clear clears it (only pushes if the alarm was active)
titlestringAlarm titleTitle shown in the pushed message
levelstringAlert levellow / mid / high / urgent
descstringDescriptionAlert detail for new; recovery note for clear

The plain-text variant (§3.3) takes a single content param and is not part of this alarm pipeline — keep it as a separate RPC for non-alarm notifications such as daily reports or reconciliation summaries.

5. Wiring into the Alert Flow

Recommended pattern: in the same trigger model, call both alarm and wecom_notify with one shared params object. alarm maintains the platform alarm record, while wecom_notify pushes the message to WeCom.

javascript
function trigger_script(device, thingModelId) {
    const ACTION = { no: "no", new: "new", clear: "clear" };
    const LEVEL  = { low: "low", mid: "mid", high: "high", urgent: "urgent" };

    let tdata = device?.telemetry_data?.[thingModelId];
    if (tdata === undefined) { return null; }

    let name   = "overheat_alarm";
    let title  = "Overheat: [" + device.name + "]";
    let level  = LEVEL.high;
    let desc   = "";
    let action = ACTION.clear;     // default: clear (condition not met)
    let group  = device?.server_attrs?.group ?? {};

    if (tdata.temperature !== undefined && tdata.temperature >= device.server_attrs?.alarm_temp) {
        desc   = "[" + device.name + "] temperature " + tdata.temperature + "°C exceeded threshold " + device.server_attrs.alarm_temp + "°C";
        action = ACTION.new;       // condition met: raise alarm
    }

    // One params object feeds both alarm and wecom_notify
    let alarmParams = {
        _eui:   device.eui,
        action: action,
        name:   name,
        title:  title,
        level:  level,
        desc:   desc,
        group:  group
    };

    return {
        delayms: 0,
        abort_previous_timer: true,
        actions: [
            { method: "alarm",        params: alarmParams },
            { method: "wecom_notify", params: alarmParams }
        ]
    };
}

⚠️ If you observe that clear actions reach wecom_notify with alarms[name] already undefined (i.e. the platform ran alarm first, removed the record, and wecom_notify cannot see it anymore), swap the actions order so wecom_notify runs first, or record the previous active state yourself via device.server_attrs and use that for the decision.

6. Push via Smart Bot

Smart bots also use a webhook URL, but management is completely different from group bots:

Group BotSmart Bot
Creation entryIn a group → Group Settings → Group Bots → AddWeCom Admin Console → App Management → Smart Bot
Permission requiredAny group memberCorp admin
ScopeA single groupCan be added to multiple groups, centrally managed
Message typestext / markdown / image / news / file / template_cardSame + interactive template-card callbacks
Receive user messages✅ (configurable callback URL)

6.1 Create a Smart Bot

  1. Open WeCom Admin Console → App Management → Smart Bot → Add Bot
  2. Configure name, avatar, and visibility scope
  3. Add the bot to every group that should receive alerts (one bot can join many groups)
  4. Copy the Webhook URL from the bot detail page — same format as a group bot:
    https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<SMART-BOT-KEY>

6.2 RPC Script

The send API is identical to a group bot — replace the wecom_webhook_url value in Org Params with the smart-bot webhook URL; the §3.4 script needs no code changes, and one RPC will now post to every group this bot has joined.

To keep both a group bot and a smart bot live at the same time, register two keys in Org Params (e.g. wecom_webhook_group and wecom_webhook_smart) and let the caller pick one via params.channel:

javascript
let webhook = params.channel === "smart"
    ? org_params?.wecom_webhook_smart
    : org_params?.wecom_webhook_group;

6.3 Security Notes

  • The key still lives only in Org Params (§2.2), never inside script bodies
  • A smart-bot key has a larger blast radius than a group-bot key — it may be present in many groups simultaneously, so a leak spams all of them. Have an admin rotate it from the console at least quarterly and update the Org Param value

7. Push via Self-Built App

When alerts need to reach specific users, departments, or tags (rather than a fixed group), the self-built app message API is the right choice.

7.1 Prerequisites

  1. WeCom Admin Console → App Management → Self-Built → Create App
  2. Record three values:
    • corpid — from the "My Company" page
    • agentid — "AgentId" field on the app detail page
    • corpsecret — "Secret" field on the app detail page
  3. Set the app's visibility scope (only members in scope can receive messages)

⚠️ A leaked corpsecret lets an attacker impersonate this app and send arbitrary messages to every member in scope. Store it in Org Params per §2.2 — never in server_attrs, trigger models, or anywhere a caller can read.

Register the three values in System Management → Server Configuration → Org Params:

KeyExampleRemark
wecom_corp_idww1234567890abcdefWeCom corpid
wecom_corp_secretxxxxxxxxxxxxxxxxxxxxxxxxxxxxSelf-built app secret; rotate quarterly
wecom_agent_id1000002Self-built app AgentId

7.2 Call Flow

Sending one message requires two HTTP calls:

  1. Get access_token: GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=...&corpsecret=...
  2. Send message: POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=...

Because step 2 depends on step 1's response, the RPC must await axios() for the token inside the script body, then return the send action (see RPC Model §1.3.1 axios input).

7.3 RPC Script

javascript
async function rpc_script({ device, params, alarms, logger, org_params }) {
    let corpId     = org_params?.wecom_corp_id;
    let corpSecret = org_params?.wecom_corp_secret;
    let agentId    = org_params?.wecom_agent_id;
    if (!corpId || !corpSecret || !agentId) {
        logger.error("wecom_corp_id / wecom_corp_secret / wecom_agent_id not configured in org_params");
        return null;
    }

    let name   = params.name;
    let action = params.action;
    let title  = params.title || name || "";
    let level  = params.level || "mid";
    let desc   = params.desc  || "";

    if (action === "clear") {
        if (!alarms || alarms[name] === undefined) return null;
    } else if (action !== "new") {
        return null;
    }

    // 1) Fetch access_token first
    let tokenResp = await axios({
        method: "GET",
        url:    "https://qyapi.weixin.qq.com/cgi-bin/gettoken",
        params: { corpid: corpId, corpsecret: corpSecret },
        timeout: 5000
    });
    let token = tokenResp.data?.access_token;
    if (!token) {
        logger.error("get wecom access_token failed", { resp: tokenResp.data });
        return null;
    }

    let header = action === "new" ? "## Device Alarm" : "## Alarm Recovered";
    let content = [
        header,
        `**Device**: ${device.name} (\`${device.eui}\`)`,
        `**Alarm**: ${title}`,
        `**Level**: ${level.toUpperCase()}`,
        `**${action === "new" ? "Description" : "Recovery"}**: ${desc || "n/a"}`,
        `**Time**: ${new Date().toISOString()}`
    ].join("\n\n");

    // 2) Return the send action (fire-and-forget via the axios action type)
    return [{
        sleepTimeMs: 0,
        type: "axios",
        dnMsg: {
            method: "POST",
            url:    `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${token}`,
            headers: { "Content-Type": "application/json" },
            data: {
                touser:  params.touser  || "@all",
                toparty: params.toparty,
                totag:   params.totag,
                msgtype: "markdown",
                agentid: Number(agentId),
                markdown: { content: content }
            },
            timeout: 5000
        }
    }];
}

7.4 Recipient Parameters

touser / toparty / totag are passed in by the caller via params. At least one must be set:

FieldTypeDescription
touserstringUser-ID list, multiple separated by |. @all means everyone in the app's visibility scope
topartystringDepartment-ID list, multiple separated by |
totagstringTag-ID list, multiple separated by |

7.5 access_token Caching Strategy

The access_token is valid for 7200 seconds, and WeCom enforces a rate limit on /gettoken. Two strategies:

  • Fetch on every alarm (used by the script above): simplest. Fine for low-volume alarms (a few dozen per day)
  • Cache and reuse: persist the token and expiry into the device's server_attrs via a type: "modifyAttrs" action; on the next call, read device.server_attrs.wecom_token first and only re-fetch if expired. Enable this when alarm volume is high to drastically cut /gettoken calls

8. Troubleshooting

SymptomLikely Cause
No messages reach the groupWrong key; bot removed from group; the TKL server cannot reach qyapi.weixin.qq.com
RPC log shows errcode != 0Message format violates WeCom spec; @user phone number format is invalid
Occasional missed messagesWeCom rate limit (max 20 messages per minute per webhook)
Self-built app errcode = 40014access_token is expired or invalid — verify caching logic handles expiry
Self-built app errcode = 60011A user/department/tag in touser / toparty / totag is outside the app's visibility scope
Self-built app errcode = 45033Hit the /gettoken rate limit — switch to the §7.5 caching strategy

9. Notes

  • Keep each message body under ~2048 characters
  • Never include customer PII, full identity numbers, tokens, or other sensitive business data in alert messages — only enough to triage the issue
  • When one bot is shared across purposes, prefix messages with an origin tag (e.g. [PROD], [STAGING])
  • For critical alerts, configure multiple notification channels (e.g. WeCom + email) so a single-channel outage does not silence alerts