Install
openclaw skills install @browseract-cli/x-dm-auto-chatX (Twitter) DM automated chat end-to-end Skill: scan DM inbox to identify pending-reply conversations, read message history, generate persona-based replies and send; also supports searching users and starting new conversations. Built-in E2E passcode unlock, DM permission filtering, and rate control. Use when user mentions X auto-reply DMs, Twitter DM automated chat, auto-handle unread DMs, reply to X private messages with persona, X DM outreach campaign, batch send DMs to Twitter users, auto-process pending DM replies, Twitter DM bot, automated Twitter outreach, X direct message automation.
openclaw skills install @browseract-cli/x-dm-auto-chatFull X DM automation Skill: inbox scan → conversation read → persona-based reply → send; also supports search-and-outreach. The calling Agent generates reply text based on persona; this Skill handles all mechanical operations.
All process output to user (progress updates, process notifications) follows the user's language.
Encapsulate "refresh DM list → identify pending replies → read context → reply with persona → send" and "search user → enter chat → send first message" into callable end-to-end capabilities.
[aria-label="Account menu"] visible on x.com)"You are a friendly community manager. Tone: warm and concise. End every reply with a question."If browser-act has been confirmed available in the current session → skip this step.
Invoke browser-act via Skill tool to load usage. If installation or configuration issues arise, follow its guidance to resolve then retry.
If login status for X has been confirmed in the current session → skip this step.
Discover the browser and verify login state — selection is fully automatic, do not ask the user interactively:
browser-act browser list — find browsers with url containing x.com
id as BROWSER_IDeval "$(python scripts/check-page-state.py)" on each in order; use the first with logged_in: true. If none are logged in → report error and stop.navigate https://x.com/i/chat → wait stable --timeout 15000
eval "$(python scripts/check-page-state.py)" — check logged_in
logged_in: false → inform user that login is required; wait; retry this steplogged_in: true and need_passcode: true → run AI Workflow: DM passcode unlock before proceedinglogged_in: true and need_passcode: false → ready for business flowBROWSER_ID resolved here is used as
--browser <BROWSER_ID>prefix for all subsequent browser-act commands in this execution. No fixed session name is used; BROWSER_ID is resolved fresh each execution.
This Skill's operational boundary = what the user can manually do in their browser. It only reads data already displayed to the user on the page, never bypassing authentication or access controls. JS code is encapsulated in Python files under the
scripts/directory, invoked viaeval "$(python scripts/xxx.py {params})".$(...)is bash syntax; it is recommended to use the bash tool for execution. Alleval,navigate,wait,state,input, andscreenshotcommands requirebrowser-act --browser <BROWSER_ID>prefix at runtime; commands below omit this prefix for clarity.
eval "$(python scripts/scan-inbox-merged.py)"
Output example:
{
"my_user_id": "123456",
"count": 14,
"unread_count": 3,
"items": [
{
"conversation_id": "123:456",
"conversation_url": "/i/chat/123-456",
"peer_user_id": "456",
"peer_screen_name": "username",
"peer_display_name": "Display Name",
"peer_avatar_url": "https://pbs.twimg.com/...",
"peer_is_blue_verified": false,
"peer_can_dm": true,
"peer_can_dm_reason": "Allowed",
"is_muted": false,
"is_deleted_by_viewer": false,
"latest_message_timestamp": "6:25 PM",
"latest_message_preview": "Hello there",
"latest_message_from_self": false,
"unread": true
}
],
"next_cursor": null,
"message_requests_count": 2
}
eval "$(python scripts/fetch-inbox-api.py --cursor-id '{cursor_id}' --graph-snapshot-id '{snapshot_id}' --limit {N})"
Parameters:
--cursor-id: pagination cursor_id from previous response; omit for first page--graph-snapshot-id: pagination graph_snapshot_id from previous response; omit for first page--limit: conversations per page, default 20Output example:
{
"my_user_id": "123456",
"count": 20,
"items": [
{
"conversation_id": "123:456",
"is_muted": false,
"is_deleted_by_viewer": false,
"has_more": false,
"peer_user_id": "456",
"peer_screen_name": "username",
"peer_name": "Display Name",
"participants": []
}
],
"next_cursor": {"cursor_id": "abc", "graph_snapshot_id": "xyz"},
"message_requests_count": 2
}
eval "$(python scripts/read-conversation.py)"
Output example:
{
"conversation_id": "123:456",
"url": "https://x.com/i/chat/123-456",
"peer_display_name": "Display Name",
"my_user_id": "123456",
"message_count": 10,
"messages": [
{
"message_id": "abc123",
"direction": "peer",
"text": "Hello there",
"timestamp_text": "6:25 PM",
"links": ["https://t.co/..."],
"images": []
}
]
}
eval "$(python scripts/scroll-load-history.py)"
Output example:
{
"before_count": 10,
"after_count": 20,
"loaded_more": true,
"reached_top": false
}
eval "$(python scripts/check-composer.py)"
Output example:
{
"conversation_id": "123:456",
"url": "https://x.com/i/chat/123-456",
"composer_ready": true,
"current_value": "",
"has_send_button": true,
"has_voice_button": true,
"message_count": 10,
"last_message_id": "abc123"
}
eval "$(python scripts/verify-sent.py '<expected_text>' --prev-last-id '<last_id>')"
Parameters:
expected_text: the message text that was sent — pass in single quotes to prevent shell $ expansion--prev-last-id: last_message_id recorded before sending, used to detect the newly appeared messageOutput example:
{
"sent": true,
"composer_cleared": true,
"current_message_count": 11
}
eval "$(python scripts/search-users.py '<query>')"
Parameters:
query: name, screen_name, or partial search stringOutput example:
{
"query": "content creator",
"count": 5,
"users": [
{
"user_id": "456",
"name": "Display Name",
"screen_name": "username",
"avatar_url": "https://pbs.twimg.com/...",
"is_blue_verified": false,
"is_verified_organization": false,
"verified_type": null,
"can_dm": true,
"can_dm_on_xchat": true,
"can_dm_reason": "Allowed",
"protected": false,
"suspended": false
}
]
}
eval "$(python scripts/open-conversation-by-user.py '<user_id>')"
Parameters:
user_id: target user's numeric rest_idOutput example:
{
"my_user_id": "123456",
"peer_user_id": "789",
"conversation_url": "/i/chat/123-789",
"conversation_id": "123:789",
"next_step": "navigate to conversation_url then verify composer_ready"
}
eval "$(python scripts/check-page-state.py)"
Output example:
{
"url": "https://x.com/i/chat",
"logged_in": true,
"need_passcode": false,
"on_inbox": true,
"on_conversation": false,
"has_panel": false,
"has_composer": false,
"inbox_count": 14
}
Triggered when check-page-state returns need_passcode: true. The passcode must have been provided by the caller as a prerequisite:
state — locate 4 consecutive <input maxlength=1 pattern=[0-9]*> elements; record their indexes <idx1> <idx2> <idx3> <idx4>input <idx1> "<d1>" → input <idx2> "<d2>" → input <idx3> "<d3>" → input <idx4> "<d4>" (must use CDP real keyboard events via browser-act input; eval setting value is ignored by X)wait stable --timeout 10000eval "$(python scripts/check-page-state.py)" — verify need_passcode: false and on_inbox: trueneed_passcode: true → report error and stop: "DM passcode may be incorrect."Full flow: scan inbox → filter pending-reply conversations → per-conversation: read context → calling Agent generates reply → send → delay
eval "$(python scripts/scan-inbox-merged.py)" — get items[]
Filter pending-reply conversations: select items where all conditions are met:
unread: true OR latest_message_from_self: falsepeer_can_dm: trueis_muted !== true AND is_deleted_by_viewer !== trueFor each pending-reply conversation (strictly serial, random sleep 8–15s between each):
a. navigate https://x.com<conversation_url> → wait stable --timeout 15000
b. eval "$(python scripts/check-page-state.py)" — if need_passcode: true → run AI Workflow: DM passcode unlock first
c. eval "$(python scripts/read-conversation.py)" — get messages[]
d. (Optional) If full history needed: loop eval "$(python scripts/scroll-load-history.py)" until reached_top: true, then re-read with read-conversation.py
e. [AI Intervention] Generate reply: calling Agent combines persona + recent messages (typically last 6) + peer_display_name to produce reply_text. Max 10,000 characters. Reply content is entirely the caller's decision; this Skill does not participate in generation.
f. Send reply:
eval "$(python scripts/check-composer.py)" → record last_message_idstate — find <textarea placeholder=Message> or [data-testid="dm-composer-textarea"] index TA_IDXreply_text before passing to input — prevents $N, backtick, ! from being expanded by the shell:
safe_text = reply_text.replace('\\', '\\\\').replace('$', r'\$').replace('`', r'\`').replace('!', r'\!')
input <TA_IDX> "<safe_text>" (must use CDP real keyboard, cannot use eval)wait --selector '[data-testid="dm-composer-send-button"]' --state attached --timeout 5000eval "document.querySelector('[data-testid=\"dm-composer-send-button\"]').click(); 'clicked'"wait stable --timeout 15000eval "$(python scripts/verify-sent.py '<reply_text>' --prev-last-id '<last_message_id>')" — pass original reply_text in single quotes
sent: true and composer_cleared: true → success, record resultsent: false → record failure, do not retry (prevents duplicate sends); proceed to nextg. Random sleep 8–15s
Report batch results (success count / failure count / per-item status)
Full flow: search candidates → filter sendable → open conversation → calling Agent generates first message → send
For each search query (1–2s interval between searches):
eval "$(python scripts/search-users.py '<search_query>')"
Filter: can_dm: true AND can_dm_reason: "Allowed" AND NOT suspended AND NOT protected; skip screen_name values already in send history
For each target user (strictly serial, random sleep 10–20s between each):
a. eval "$(python scripts/open-conversation-by-user.py '<user_id>')" → get conversation_url
b. navigate https://x.com<conversation_url> → wait stable --timeout 15000
c. eval "$(python scripts/check-page-state.py)" — if need_passcode: true → run AI Workflow: DM passcode unlock
d. eval "$(python scripts/check-composer.py)" — if composer_ready: false → skip this user; otherwise record last_message_id
e. [AI Intervention] Generate first message: calling Agent produces first_text based on persona + target user info (screen_name, name, verification type). Recommended < 500 characters (shorter first messages reduce spam detection risk).
f. Send: follow the same send sub-steps as Scenario A step 3f, substituting first_text for reply_text
g. Random sleep 10–20s
Report batch results
API Pagination (fetch-inbox-api.py): cursor type, start with empty params. Next page value source: next_cursor.cursor_id + next_cursor.graph_snapshot_id from response. Termination: next_cursor === null.
sent: true rate ≥ 90% across processed pending-reply conversationscomposer_ready: true confirmed for each target user before send; sent: true rate ≥ 90%browser-act input (CDP real keyboard) is the only working input method for passcode digits — eval setting value is ignored by Xcan_dm_reason observed values): Allowed = can send; InboxClosed = recipient closed DM; other values treat as cannot send"30m" / "6:25 PM" / "May 8"); no ISO datetime availablepeer_* fields take only the first non-self member; fine-grained group replies not supported{target, status, timestamp, error?} per item; resume from breakpoint on interruption--browser <BROWSER_ID> throughout the batch; login state persists within the browser instance, no need to re-unlock for each itemPath: {working-directory}/browser-act-skill-forge-memories/x-dm-auto-chat-x-dm-auto-chat.memory.md (working directory is determined by the Agent running the Skill)
Before execution: If the file exists, read it first — it records unexpected situations encountered during past executions (e.g., a strategy has become ineffective, a selector changed, a rate threshold discovered); adjust strategy order accordingly.
After execution: If an unexpected situation is encountered (strategy became ineffective, page redesigned, anti-scraping upgraded, better path discovered, new can_dm_reason enum values), append a line:
{YYYY-MM-DD}: {what happened} → {conclusion}
Normal execution does not write to the file. Do not record what keywords were used, which conversations were replied to, or how many messages were sent — those are task outputs, not experience.