Topic calendar to WeChat drafts: Feishu → WeChat MP
Feishu due dates open approval, then land as a WeChat official-account draft.
Feishu Bitable fires once when a row hits its date — use a 送审 (send-for-review) date ahead of publish. That automation POSTs to your HTTPS endpoint; the backend opens a Feishu Approval. Completion arrives on Feishu's HTTP callback (SDK long-connection: one-line alt for this event only), after a one-time subscribe per definition. On APPROVED, cover → permanent WeChat material → draft; a person still sends. No native bridge. Prefer an enterprise-authenticated account — draft-box APIs tightened since mid-2025; verify with a real draft/add.
How it flows
- 01
Calendar row reaches its send-for-review date
Bitable automation fires once for that record — gated so 状态 (status) is still 待审批 — and POSTs the record id to the backend HTTPS endpoint.
- 02
Backend opens the approval instance
Reads 选题标题 (title) and 正文 (body), POSTs them into approval/v4/instances (form is a JSON-array string keyed by widget ids; initiator is the owner's user_id), then writes instance_code and 状态 (status)=审批中 back to the row.
- 03
Reviewer decides in Feishu Approval
The approver acts in Feishu's own approval UI. Nothing touches WeChat until this returns a decision.
- 04
Approval status-change event reaches the backend
Delivered on the HTTP callback (or the long-connection client, if that is the path you chose for this event). Continue only when status == APPROVED; REJECTED → 已拒绝, CANCELED → 已撤回, and stop without calling WeChat.
- 05
Cover file_token becomes image bytes
Exchange the attachment file_token for a 24-hour temporary download URL, then pull the image bytes.
- 06
Cover uploads as permanent WeChat material
POST material/add_material?type=image; the returned media_id becomes thumb_media_id for the draft.
- 07
Draft is created
POST draft/add with title, digest, content, and thumb_media_id. Write the returned draft media_id and 状态 (status)=草稿已生成 back to the calendar row.
- 08
A person reviews and sends
Automation stops at the draft box. Someone opens the WeChat backend, reviews the draft, and sends only after confirming — this workflow never calls freepublish/submit.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
Feishu
Holds the topic calendar, fires on the due date, and runs the approval
- 01
Create the self-built app and wire it to the calendar
open.feishu.cn → 开发者后台 (Developer Console) → create a 企业自建应用 (custom enterprise app); note the App ID and App Secret. (Menu labels here are the Chinese console; Feishu follows your account language, so switch it if yours is already English.) Under 权限管理 (Permissions) add `bitable:app` (read/write Bitable records), plus one of `approval:approval` or `approval:instance` (create instances) and one of `approval:approval` or `approval:definition` (subscribe to approval events) — `approval:approval` alone covers both if the console offers it. Publish a version, then have a tenant admin approve the app in 管理后台 (Admin Console) → 应用管理 (App Management) → this app → 权限管理 (Permissions): a self-built app's requested scopes sit inactive until an admin approves them there. A new app has zero access to any base until you open the target topic-calendar Bitable's share/more menu and add this app as a collaborator. Add these fields if missing: 选题标题 (title, text), 正文 (body, long text), 封面图 (cover, attachment), 送审日期 (send-for-review date, date), 计划发布日期 (planned publish date, date), 状态 (status, single select: 待审批 (pending review)/审批中 (in review)/已批准 (approved)/已拒绝 (rejected)/已撤回 (canceled)/草稿已生成 (draft created)), 审批实例ID (instance id, text), 草稿media_id (draft media id, text).
- 02
Build the approval definition
In the approval management console (approval.feishu.cn, or the 审批 (Approval) app's admin entry inside the Feishu client) create a new definition — e.g. '内容选题发布审批' — with at least a title input and a body/summary textarea, and a fixed approver for now (department-hierarchy routing is more setup than this needs). Copy the `approval_code` from the definition's detail-page URL, and read each control's `id` from the form builder — the create-instance call addresses controls by that id, not by the on-screen label. `user_id` on the create-instance call is the initiator's Feishu user_id (readable from 通讯录 (Contacts) / 管理后台 (Admin Console); this flow has no cross-product identity mapping).
POST /open-apis/approval/v4/instances — body shape (flow time){ "approval_code": "YOUR_APPROVAL_CODE", "user_id": "INITIATOR_USER_ID", "form": "[{\"id\":\"widget_title\",\"type\":\"input\",\"value\":\"Next week topic: XXX\"},{\"id\":\"widget_summary\",\"type\":\"textarea\",\"value\":\"Body summary...\"}]" } - 03
Turn on the due-date automation
On the calendar Bitable, open 自动化 (Automation) → new automation → trigger 到达记录中的时间时 (when a record reaches its time) → point it at 送审日期 (send-for-review date). Add a 逻辑 (Logic) condition that 状态 (status) is still 待审批, so a pulled or already-published row does not empty-fire. Action: 发送 HTTP 请求 (Send HTTP Request) to your backend receiving URL, with the record id in the payload — this hop needs a public HTTPS endpoint. The trigger fires exactly once per record at that field's value; it is not a recurring scan. Feishu's docs do not say whether editing 送审日期 (send-for-review date) afterward reschedules the pending fire or leaves the original time queued — treat a later date edit as unverified until you test it once on a real record.
- 04
Subscribe to approval completion over HTTP
开发者后台 (Developer Console) → 事件与回调 (Events & Callbacks): subscribe to 审批实例状态变更 (approval instance status changed) and point it at an HTTPS callback URL on your backend — the primary path for approval completion. (One-line alternative for this event only: Feishu's server-side SDK long-connection client pulls the same event over an outbound connection, with no inbound approval callback; the Bitable due-date POST above still needs its HTTPS endpoint either way.) Console subscription alone is not enough: call the endpoint below once per approval definition, or the event type stays enabled while this definition never sends.
POST approval/v4/approvals/{approval_code}/subscribe (once per definition)curl -s -X POST "https://open.feishu.cn/open-apis/approval/v4/approvals/${APPROVAL_CODE}/subscribe" \ -H "Authorization: Bearer $FEISHU_TENANT_TOKEN" # empty body; once per approval_code, not per instance # tenant_access_token: POST open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal # with {"app_id","app_secret"} - 05
Exchange the cover file_token for a download URL
The 封面图 (cover) attachment field only returns a `file_token`, never a URL. Before WeChat can receive the image bytes, exchange it for a 24-hour temporary download link with the call below (same `tenant_access_token` as the other Feishu calls). If `batch_get_tmp_download_url` returns a permission error, add the Drive media-related scopes under 权限管理 (Permissions) and retry (exact scope names PENDING-WEB).
GET drive/v1/medias/batch_get_tmp_download_urlcurl -s -G "https://open.feishu.cn/open-apis/drive/v1/medias/batch_get_tmp_download_url" \ --data-urlencode "file_tokens=${FILE_TOKEN}" \ -H "Authorization: Bearer $FEISHU_TENANT_TOKEN" # returned URL is valid for 24 hours; FILE_TOKEN comes from the Bitable attachment field
WeChat MP
Turns the approved item into a draft — outbound only
- 01
Confirm the account can create drafts
mp.weixin.qq.com → 开发 (Development) → 基本配置 (Basic Configuration): note AppID and AppSecret, and check the account's authentication status. This workflow never calls `freepublish/*` (those publish APIs are documented as restricted to enterprise-authenticated accounts since July 2025). `draft/add` and `add_material` are not named on that same restriction footnote, but community reports treat them as covered in practice. Before production, make one real `draft/add` call from your actual account and confirm it does not return an authorization error — do not assume an unauthenticated account works. No Server URL / Token setup is needed here: this seed only writes outbound to the draft box and never receives WeChat inbound callbacks.
- 02
Draft box is fully available (switch API deprecated)
The draft box and publish features are fully rolled out — the old `/cgi-bin/draft/switch` enablement API is deprecated, so production does not need to query a switch. If you still call the legacy endpoint, use `checkonly=1` rather than `action=get`.
- 03
Get a stable access token
Use `stable_token`, not the older `token` endpoint — the old one invalidates the previous token on every refresh, so concurrent callers kick each other out. With `force_refresh: false`, `stable_token` returns the still-valid cached token. Valid for 7200 seconds.
POST https://api.weixin.qq.com/cgi-bin/stable_token{ "grant_type": "client_credential", "appid": "YOUR_APPID", "secret": "YOUR_APPSECRET", "force_refresh": false } - 04
Upload the cover as permanent material, then create the draft
Upload first (`type=image`, ≤10MB, bmp/png/jpeg/jpg/gif) — the draft's `thumb_media_id` must be a permanent MediaID from this call, not a temporary upload. Then `draft/add`: `title` ≤32 characters, `digest` ≤120, `content` ≤20,000 characters and <1M with basic HTML. The draft response's `media_id` is a different id from `thumb_media_id` — do not swap them. Stop here; do not call `freepublish/submit`.
add_material (image) then draft/add# 1) permanent cover curl -s -X POST "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${ACCESS_TOKEN}&type=image" \ -F "media=@cover.jpg" # {"media_id":"COVER_MEDIA_ID","url":"https://..."} # 2) draft (thumb_media_id = COVER_MEDIA_ID from step 1) curl -s -X POST "https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "articles": [{ "title": "Next week topic: XXX", "author": "Content team", "digest": "Body summary, under 120 characters", "content": "<p>Body HTML...</p>", "thumb_media_id": "COVER_MEDIA_ID", "need_open_comment": 1, "only_fans_can_comment": 0 }] }' # {"media_id":"DRAFT_MEDIA_ID"}