X Post ExecModule
Overview
XPostModule (social.x.post) creates one Post through POST /2/tweets on the X API host. It supports text, replies, quotes, and up to four media IDs returned by a completed X media upload.
Version 2.0 removes the former placeholder media path. The module never downloads arbitrary media_urls, never invents a mock_media_* identifier, and never reports success unless X returns a valid data.id. Upload media through the X media API first and pass the resulting IDs to this module.
Post creation is an outbound, non-idempotent action. The module validates every field before the provider request and does not automatically retry HTTP 429, 5xx, timeout, or ambiguous network failures.
Usage
- Create or select an X developer App that can perform user-context Post writes.
- Authorize the publishing identity with an OAuth 2.0 PKCE user access token.
- Store that token in the encrypted
apiKeyfield of a ValkyrAIIntegrationAccountand bind the account throughExecModuleConfig.authConfig.integrationAccount. - Add
XPostModuleto a workflow and set non-emptytext. - Optionally set one reply ID, one quote ID, or one to four pre-uploaded media IDs. X does not allow
media_idsandquote_tweet_idtogether. - Require outbound approval where your workflow policy demands it, then inspect the returned Post ID before continuing.
Inputs
| Name | Type | Required | Description | Constraints |
|---|---|---|---|---|
text | string | Yes | Text sent to X. | Non-blank and at most 280 Unicode code points locally. X applies final weighted-length and product-rule validation. |
media_ids | array of strings | No | Media IDs from completed X media uploads. | At most four numeric IDs. Cannot be combined with quote_tweet_id. |
reply_to_tweet_id | string | No | Existing Post ID to reply to. | Numeric X identifier. Legacy camel-case replyToTweetId is accepted at runtime. |
quote_tweet_id | string | No | Existing Post ID to quote. | Numeric X identifier. Mutually exclusive with media_ids. |
media_urls is not supported. A non-empty legacy value fails before any X request instead of producing a fabricated media ID.
Outputs
| Name | Type | When present | Description |
|---|---|---|---|
x.post.tweet_id | string | Success | Numeric identifier returned in X data.id. |
x.post.permalink | string | Success | https://x.com/i/web/status/<id> URL for the created Post. |
x.post.status | string | Always | POSTED on verified success or ERROR on failure. |
The module also marks the runtime ExecModule GOOD or ERROR and emits bounded EventLog progress. Access tokens and provider response bodies are never written to workflow state or failure events.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | X API v2 |
| Authentication | OAuth 2.0 Authorization Code with PKCE user context |
accountName | Human-readable publishing identity |
apiKey | Encrypted X user access token used as Authorization: Bearer ... |
| Relationship | Bind through ExecModuleConfig.authConfig.integrationAccount |
An OAuth client ID, client secret, or app-only bearer token is not a substitute for the authorized user access token needed to create a Post. Do not place credentials in payload parameters, Post text, logs, or documentation examples.
Configuration
Illustrative normalized configuration:
{
"version": "2.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:x-publishing-user"
},
"payloadConfig": {
"parameters": "{\"text\":\"Launch notes are live.\",\"media_ids\":[\"1880028106020515840\"],\"reply_to_tweet_id\":\"1445880548472328192\"}"
}
}
The account value is a symbolic secure reference. Persisted workflows use the generated IntegrationAccount relationship, not a plaintext token.
The default transport uses a 10-second connection timeout and a 30-second read timeout. Post creation is not retried automatically, even when a workflow retry policy exists, because an ambiguous failure may have committed the Post.
Operations
XPostModule has one operation with four compatible shapes:
| Shape | Provider payload | Side effect |
|---|---|---|
| Text Post | text | Creates one public or account-policy-governed Post. |
| Reply | text plus reply.in_reply_to_tweet_id | Creates one reply. |
| Quote | text plus quote_tweet_id | Creates one quote Post. |
| Media Post | text plus media.media_ids | Creates one Post using already-uploaded media. |
Media upload, delete, edit, poll, card, geo, scheduling, analytics, and bulk publishing are separate operations and are not implemented by this module.
Errors and Failure Modes
| Failure | Cause | Retry guidance |
|---|---|---|
| Validation failure | Missing text, more than 280 code points, malformed IDs, more than four media IDs, media plus quote, or non-empty media_urls. | Correct the configuration; no X request was sent. |
| IntegrationAccount failure | No bound account or blank apiKey. | Bind a valid user-context X account; no X request was sent. |
| HTTP 400/403/404 | Provider validation, product policy, scope, identity, or referenced-Post/media problem. | Correct the account or payload before a new attempt. |
| HTTP 401 | Token is missing, expired, revoked, or unsuitable for user-context creation. | Reauthorize the X user and rotate the IntegrationAccount token. |
| HTTP 429 | X rate limit. | Reconcile whether the Post exists, honor provider reset guidance, then schedule a deliberate new attempt. |
| HTTP 5xx or network failure | Provider or transport failure with an ambiguous commit boundary. | Do not retry blindly. Check the publishing account or durable correlation context first. |
| Invalid success response | A 2xx response lacks a numeric data.id or is not valid JSON. | Treat as failure and inspect provider compatibility; success is not recorded. |
Provider response bodies can contain credential-like or user content, so failures expose only a sanitized HTTP status or bounded local validation message.
Example
Create a reply with one previously uploaded image:
{
"text": "Launch notes are live.",
"media_ids": ["1880028106020515840"],
"reply_to_tweet_id": "1445880548472328192"
}
Expected result shape after X returns 201 with a valid data.id:
{
"x.post.status": "POSTED",
"x.post.tweet_id": "1880030000000000000",
"x.post.permalink": "x.com/i/web/status/1880030000000000000"
}
Notes
- Pagination: not applicable; one execution attempts one Post creation.
- Media limits: X currently accepts one to four media IDs on the create-Post request. Upload processing and media-category constraints belong to the X media API.
- Rate limits: the module surfaces HTTP 429 without retrying. Provider limits vary by access tier and endpoint policy.
- Idempotency: Post creation has no durable idempotency key in this module. A workflow retry can create a duplicate Post, so reconcile ambiguous attempts before re-execution.
- API constraints: X performs final text weighting, scope, referenced-Post, account, policy, and media compatibility checks. Local validation is intentionally narrower.
- Destructive behavior: creation is an external outbound write. This module cannot delete or compensate a Post; removal requires a separately authorized operation.
- Observability: EventLogs record validation, submission, success ID, or sanitized failure. Tokens and provider bodies are excluded.
- Unverified boundary: deterministic tests cover account binding, exact request construction, validation, response parsing, non-retry behavior, metadata serialization, and secret redaction. Live X execution requires separately authorized service credentials and is not exercised in repository tests.
See the official create-Post reference and manage Posts quickstart for current provider behavior.