Send images inline in Feishu chat by uploading via API to get image_key, then sending image message using receive_id_type in URL query.
---
name: feishu-image-send
description: Send inline images via Feishu Bot messages. Use when the `message` tool's `filePath`/`file_path` parameters fail to render images (shows JSON text instead). Works by generating a temporary Node.js script that calls the Feishu Open API directly (upload image → get image_key → send image message). Trigger: sending images to Feishu chat, image rendering fails, or user asks to send a picture via Feishu bot.
---
# Feishu Image Send
Send images that render inline in Feishu chat (not as file links).
## Problem
The `message` tool's `filePath`/`file_path` parameters often fail for Feishu:
- API returns `ok:true` but the recipient sees raw JSON text instead of rendered image
- Caused by path restrictions (`mediaLocalRoots`) and outbound handling bugs
- This skill bypasses the issue by calling the Feishu Open API directly
## Workflow
When asked to send an image to a Feishu chat:
1. Get the image path and target user/chat
2. Generate a temporary Node.js script with values filled in (see Template below)
3. Write it to `/tmp/feishu-send-{timestamp}.js` using `write`
4. Run: `node /tmp/feishu-send-{timestamp}.js`
5. Confirm `✅ Image sent` in output, then clean up: `rm /tmp/feishu-send-{timestamp}.js`
### Script Template
Copy this template and fill in the values:
```javascript
const https = require('https');
const fs = require('fs');
// === Config: update these values ===
const APP_ID = '<app_id>'; // e.g. cli_a931e5b57ff89cc0
const APP_SECRET = '<app_secret>'; // from openclaw.json
const IMAGE_PATH = '/absolute/path/to/image.jpg'; // must be absolute
const RECEIVE_ID = '<open_id_or_chat_id>'; // e.g. ou_71c53ff7589f8527a27c2a057b96b6d7
const RECEIVE_ID_TYPE = 'open_id'; // or 'chat_id', 'user_id'
// ===================================
function req(url, opts, body) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const r = https.request({
hostname: u.hostname,
path: u.pathname + u.search,
method: opts.method || 'GET',
headers: opts.headers || {}
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => { try { resolve(JSON.parse(d)) } catch (e) { resolve(d) } });
});
r.on('error', reject);
if (body) r.write(typeof body === 'string' ? body : JSON.stringify(body));
r.end();
});
}
(async () => {
// 1. Get tenant access token
const t = await req('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, { app_id: APP_ID, app_secret: APP_SECRET });
if (t.code !== 0) { console.error('Token error:', t); process.exit(1); }
const token = t.tenant_access_token;
console.log('Token acquired');
// 2. Upload image via multipart/form-data
const boundary = '----Boundary' + Date.now().toString(36);
const CRLF = '\r\n';
const img = fs.readFileSync(IMAGE_PATH);
const fn = IMAGE_PATH.split('/').pop();
const body = Buffer.concat([
Buffer.from(`--${boundary}${CRLF}`),
Buffer.from(`Content-Disposition: form-data; name="image_type"${CRLF}${CRLF}message${CRLF}`),
Buffer.from(`--${boundary}${CRLF}`),
Buffer.from(`Content-Disposition: form-data; name="image"; filename="${fn}"${CRLF}`),
Buffer.from(`Content-Type: image/jpeg${CRLF}${CRLF}`),
img,
Buffer.from(`${CRLF}--${boundary}--${CRLF}`),
]);
const uploaded = await new Promise((resolve, reject) => {
const r = https.request({
hostname: 'open.feishu.cn',
path: '/open-apis/im/v1/images',
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length
}
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => { try { resolve(JSON.parse(d)) } catch (e) { reject(e) } });
});
r.on('error', reject); r.write(body); r.end();
});
if (uploaded.code !== 0) { console.error('Upload error:', uploaded); process.exit(1); }
const imageKey = uploaded.data.image_key;
console.log('Image uploaded, key:', imageKey);
// 3. Send image message
// ⚠️ RECEIVE_ID_TYPE must be in URL query string, NOT in JSON body
const content = JSON.stringify({ image_key: imageKey });
const sent = await req(
`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${encodeURIComponent(RECEIVE_ID_TYPE)}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json; charset=utf-8'
}
},
{ receive_id: RECEIVE_ID, msg_type: 'image', content }
);
if (sent.code !== 0) { console.error('Send error:', sent); process.exit(1); }
console.log('✅ Image sent. message_id:', sent.data.message_id);
})();
```
## Critical Rules
| Rule | Detail |
|------|--------|
| `receive_id_type` | **Must** be a URL query parameter (`?receive_id_type=open_id`), not in the JSON body |
| `image_type` in upload | Use `"message"` for chat images, not `"avatar"` |
| `content` field in send | Must be a JSON **string** (double-escaped), not an object |
| `IMAGE_PATH` | Must be an absolute path to a readable image file |
## Getting Credentials
Read `openclaw.json` → `channels.feishu.accounts.<accountId>` for `appId` and `appSecret`.
For the CTO agent, these are already known:
- `accountId`: `cto`
- `appId`: `cli_a931e5b57ff89cc0`
- `appSecret`: `Q3c78ab1ORB7xOj0JtRz2d1GwQtzZcZH`
For the target user, use the current conversation's sender `open_id` from the inbound context.
## Supported Formats
JPEG, PNG, GIF, WEBP, TIFF, BMP, ICO — max 30MB.
## Common Pitfalls
| Symptom | Cause | Fix |
|---------|-------|-----|
| `99992402 field validation failed` for `receive_id_type` | Parameter placed in JSON body instead of URL | Move `receive_id_type=open_id` to the URL query string |
| `234011 Can't recognize image format` | Corrupted, missing, or unsupported file | Ensure valid JPEG/PNG and file exists at the given path |
| Image uploads but not displayed in chat | Used `image_type=avatar` instead of `message` | Change `image_type` to `"message"` for chat images |
| `message` tool returns `ok` but no image renders | `filePath` not in `mediaLocalRoots` or outbound bug | Use this skill's direct API method instead |
## Integration with Cron Jobs
In automated reports (daily/weekly), generate and run the script programmatically:
```javascript
const { writeFileSync, unlinkSync } = require('fs');
const { execSync } = require('child_process');
const path = '/tmp/feishu-send-' + Date.now() + '.js';
const script = `/* filled template */`;
writeFileSync(path, script);
execSync(`node ${path}`);
unlinkSync(path);
```
## API Reference
- **Upload Image**: `POST /open-apis/im/v1/images` (multipart/form-data)
- **Send Message**: `POST /open-apis/im/v1/messages?receive_id_type={type}` (JSON body)
Official docs:
- [Upload Image](https://open.feishu.cn/document/ukTMukTMukTM/ukTM5UjL5ETO14COxkTN/1images-1upload)
- [Send Message](https://open.feishu.cn/document/ukTMukTMukTM/ukTM5UjL5ETO14COxkTN/1messages-1create)
don't have the plugin yet? install it then click "run inline in claude" again.
formalized intent, inputs, and procedure sections; made decision points explicit with if-else branches; documented output contract and outcome signal; added edge cases (rate limits, token expiry, network timeouts, invalid credentials); preserved template and api reference.
Send images that render inline in Feishu chat instead of as file links or broken JSON text.
This skill bypasses the message tool's image rendering failures in Feishu by calling the Feishu Open API directly. use it when the standard filePath/file_path parameters fail to render images (showing raw JSON text instead), or when you need guaranteed inline image delivery to a specific user or chat. the skill generates a temporary Node.js script, uploads the image to get an image_key, then sends an image message with that key.
Feishu API Credentials
APP_ID: application ID (e.g. cli_a931e5b57ff89cc0). read from openclaw.json under channels.feishu.accounts.<accountId>.appId, or ask the user.APP_SECRET: application secret. read from openclaw.json under channels.feishu.accounts.<accountId>.appSecret, or ask the user. keep it secret; do not log it.TENANT_ACCESS_TOKEN: generated on-the-fly via POST /open-apis/auth/v3/tenant_access_token/internal using APP_ID and APP_SECRET. expires after ~2 hours.Image and Target
IMAGE_PATH: absolute filesystem path to the image file (e.g. /home/user/photos/report.png). must be readable by the Node.js process. supports JPEG, PNG, GIF, WEBP, TIFF, BMP, ICO. max 30MB.RECEIVE_ID: the target open_id (e.g. ou_71c53ff7589f8527a27c2a057b96b6d7), chat_id (e.g. oc_e521c7b922d7feef6aec25a78a65e5a3), or user_id. extract from inbound Feishu context (message sender's open_id) or ask the user.RECEIVE_ID_TYPE: one of open_id, chat_id, or user_id. must match the type of RECEIVE_ID. defaults to open_id if the user provides only an identifier.External Connection
https://open.feishu.cn (outbound HTTPS required).Gather inputs. prompt the user for IMAGE_PATH, RECEIVE_ID, and RECEIVE_ID_TYPE if not provided. if APP_ID and APP_SECRET are not in context, read them from openclaw.json under the target Feishu account. validate that the image file exists and is readable.
Generate the script. create a Node.js script (see Template below) with the four config values substituted: APP_ID, APP_SECRET, IMAGE_PATH, RECEIVE_ID, RECEIVE_ID_TYPE. do not modify the template logic.
Write to temporary file. use the write tool to save the script to /tmp/feishu-send-{unix_timestamp}.js (e.g. /tmp/feishu-send-1704067200000.js). record the filename for cleanup.
Execute the script. run node /tmp/feishu-send-{unix_timestamp}.js using the shell tool. the script will:
/open-apis/auth/v3/tenant_access_token/internal to get a tenant access token. output: tenant_access_token in the JSON response./open-apis/im/v1/images (multipart/form-data) with image_type=message. output: image_key in the JSON response./open-apis/im/v1/messages?receive_id_type={RECEIVE_ID_TYPE} (JSON body) with msg_type=image and content={\"image_key\":\"...\"}. output: message_id in the JSON response.Verify success. check the script's stdout for the string ✅ Image sent. if present, the image was delivered. if any step fails, the script exits with code 1 and prints an error object (e.g. Token error:, Upload error:, Send error:).
Clean up. after confirming success or failure, delete the temporary script file: rm /tmp/feishu-send-{unix_timestamp}.js. do not leave temporary files behind.
if APP_ID and APP_SECRET are already in context (e.g. from openclaw.json or previous steps), use them directly. else, prompt the user to provide them or point them to openclaw.json.
if the image file does not exist or is not readable, stop and ask the user to provide a valid absolute path before generating the script.
if RECEIVE_ID_TYPE is ambiguous (user provides only an identifier without a type), infer open_id as the default; ask the user to confirm if uncertain.
if the script fails with error code 1, examine the error message. if it says Token error:, the APP_ID or APP_SECRET is invalid or the app lacks permission to generate tokens. if it says Upload error:, the image is corrupted, unsupported, or exceeds 30MB. if it says Send error:, the RECEIVE_ID is invalid, the RECEIVE_ID_TYPE is wrong, or the user lacks permission to receive the message.
if the script succeeds (exit code 0 and ✅ Image sent in stdout), the image was delivered inline to the target. no further action is needed.
Success state
✅ Image sent. message_id: <message_id>.Failure state
Token error:, Upload error:, Send error: followed by a JSON object with code and msg fields.Temporary file
/tmp/feishu-send-{unix_timestamp}.js is created, executed, and deleted.✅ Image sent. message_id: <message_id> in the terminal output./tmp/.copy this template and fill in the four config values before writing to disk:
const https = require('https');
const fs = require('fs');
// === Config: update these values ===
const APP_ID = '<app_id>'; // e.g. cli_a931e5b57ff89cc0
const APP_SECRET = '<app_secret>'; // from openclaw.json
const IMAGE_PATH = '/absolute/path/to/image.jpg'; // must be absolute
const RECEIVE_ID = '<open_id_or_chat_id>'; // e.g. ou_71c53ff7589f8527a27c2a057b96b6d7
const RECEIVE_ID_TYPE = 'open_id'; // or 'chat_id', 'user_id'
// ===================================
function req(url, opts, body) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const r = https.request({
hostname: u.hostname,
path: u.pathname + u.search,
method: opts.method || 'GET',
headers: opts.headers || {}
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => { try { resolve(JSON.parse(d)) } catch (e) { resolve(d) } });
});
r.on('error', reject);
if (body) r.write(typeof body === 'string' ? body : JSON.stringify(body));
r.end();
});
}
(async () => {
// 1. Get tenant access token
const t = await req('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, { app_id: APP_ID, app_secret: APP_SECRET });
if (t.code !== 0) { console.error('Token error:', t); process.exit(1); }
const token = t.tenant_access_token;
console.log('Token acquired');
// 2. Upload image via multipart/form-data
const boundary = '----Boundary' + Date.now().toString(36);
const CRLF = '\r\n';
const img = fs.readFileSync(IMAGE_PATH);
const fn = IMAGE_PATH.split('/').pop();
const body = Buffer.concat([
Buffer.from(`--${boundary}${CRLF}`),
Buffer.from(`Content-Disposition: form-data; name="image_type"${CRLF}${CRLF}message${CRLF}`),
Buffer.from(`--${boundary}${CRLF}`),
Buffer.from(`Content-Disposition: form-data; name="image"; filename="${fn}"${CRLF}`),
Buffer.from(`Content-Type: image/jpeg${CRLF}${CRLF}`),
img,
Buffer.from(`${CRLF}--${boundary}--${CRLF}`),
]);
const uploaded = await new Promise((resolve, reject) => {
const r = https.request({
hostname: 'open.feishu.cn',
path: '/open-apis/im/v1/images',
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length
}
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => { try { resolve(JSON.parse(d)) } catch (e) { reject(e) } });
});
r.on('error', reject); r.write(body); r.end();
});
if (uploaded.code !== 0) { console.error('Upload error:', uploaded); process.exit(1); }
const imageKey = uploaded.data.image_key;
console.log('Image uploaded, key:', imageKey);
// 3. Send image message
// CRITICAL: receive_id_type must be in URL query string, NOT in JSON body
const content = JSON.stringify({ image_key: imageKey });
const sent = await req(
`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${encodeURIComponent(RECEIVE_ID_TYPE)}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json; charset=utf-8'
}
},
{ receive_id: RECEIVE_ID, msg_type: 'image', content }
);
if (sent.code !== 0) { console.error('Send error:', sent); process.exit(1); }
console.log('✅ Image sent. message_id:', sent.data.message_id);
})();
| Rule | Detail |
|---|---|
receive_id_type |
must be a URL query parameter (e.g. ?receive_id_type=open_id), not in the JSON body. feishu will reject with error 99992402 if placed in the body. |
image_type in upload |
use "message" for chat images, not "avatar". avatar is for profile pictures. |
content field in send |
must be a JSON string (double-escaped), e.g. "{\"image_key\":\"...\"}"; not an object. |
IMAGE_PATH |
must be an absolute path. relative paths will fail during file read. |
read openclaw.json (usually in the project root or config folder) and navigate to channels.feishu.accounts.<accountId>. you will find:
appId: the APP_ID value.appSecret: the APP_SECRET value.for the default cto agent account:
appId: cli_a931e5b57ff89cc0appSecret: Q3c78ab1ORB7xOj0JtRz2d1GwQtzZcZHfor the target user, extract the open_id from the current Feishu message context (the sender's identifier in the inbound webhook or API call).
JPEG, PNG, GIF, WEBP, TIFF, BMP, ICO. max file size: 30MB. corrupted or truncated files will fail at upload with error 234011 Can't recognize image format.
| Symptom | Cause | Fix |
|---|---|---|
error 99992402 field validation failed for receive_id_type |
parameter placed in JSON body instead of URL query string | move receive_id_type=open_id to the URL query string (after the ?). |
| error 234011 Can't recognize image format | corrupted, missing, or unsupported file | ensure the file is a valid JPEG, PNG, etc. and exists at the given absolute path. check file size (max 30MB). |
| image uploads but not displayed in chat | used image_type=avatar instead of message, or feishu api rate limit hit (60 reqs/min per app) |
set image_type to "message". if rate limited, wait 60 seconds and retry. |
message tool returns ok:true but no image renders |
filePath not in mediaLocalRoots or bug in outbound path handling |
use this skill's direct API method instead. |
| no response or timeout | network issue, firewall, or feishu api server down | check network connectivity. try again in a few seconds. if persistent, check feishu api status. |
| token error, code 40001 | APP_ID or APP_SECRET is invalid, or app lacks permission |
verify credentials in openclaw.json. confirm the app is active in feishu admin console. |
| send error, target user not found | RECEIVE_ID is invalid or not an open_id/chat_id/user_id |
ask the user to provide the correct identifier. use feishu's user directory to look up the open_id. |
to send images programmatically in automated reports (daily/weekly summaries, alerts, etc.), generate and run the script inside your job:
const { writeFileSync, unlinkSync } = require('fs');
const { execSync } = require('child_process');
const path = '/tmp/feishu-send-' + Date.now() + '.js';
const script = `/* filled template from above */`;
writeFileSync(path, script);
try {
execSync(`node ${path}`, { stdio: 'inherit' });
} finally {
unlinkSync(path);
}
POST /open-apis/auth/v3/tenant_access_token/internal. body: {app_id, app_secret}. response: {code, data: {tenant_access_token}}. expires in ~2 hours.POST /open-apis/im/v1/images. headers: Authorization: Bearer <token>, Content-Type: multipart/form-data. body: multipart with fields image_type (text) and image (file). response: {code, data: {image_key, ...}}.POST /open-apis/im/v1/messages?receive_id_type={type}. headers: Authorization: Bearer <token>, Content-Type: application/json. body: {receive_id, msg_type: "image", content: "{\"image_key\":\"...\"}\"}. response: {code, data: {message_id, ...}}.official feishu docs:
credits. original skill by jamesqin-cn. enriched for implexa standards.