> ## Documentation Index
> Fetch the complete documentation index at: https://www.openpmm.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Draft and publish

> Create drafts, publish Posts, and inspect publishing status with the OpenPMM CLI.

A draft is a Post with the `draft` state. Publishing changes the same Post.

## Create a draft

```bash theme={null}
openpmm posts create \
  --workspace ws_01JABCDEF \
  --when draft \
  --group launch-2026-08 \
  --channel x \
  --body 'First post in the thread' \
  --body 'Second post in the thread' \
  --media-item '1:ast_01JABCDEF' \
  --json
```

Store the returned Post `id` and `version`.

Repeat `--body` to create an ordered self-reply chain for X, Bluesky, Mastodon,
or Threads. Repeat `--media-item <body-index>:<asset-id>` to attach media to a
specific body item. The first body index is `0`. The other channels accept one
body item. On X, Bluesky, Mastodon, and Threads, any body item can contain media
without text. Keep an empty `--body ''` at that thread position and attach at
least one Asset to the same index.

```bash theme={null}
openpmm posts create \
  --when draft \
  --channel threads \
  --body 'Opening post' \
  --body '' \
  --media-item '1:ast_01JABCDEF' \
  --json
```

For X, every body item uses the selected Destination's effective limit.
Read it with `openpmm destinations show <destination-id> --json`.
Run that command before you write or schedule long copy.
Refresh Destinations after an X subscription change.

For a direct or scheduled Post, use `--destination` instead of `--channel`.
Put provider-specific settings in `--destination-options`. For example:

```bash theme={null}
openpmm posts create \
  --workspace ws_01JABCDEF \
  --destination dest_01JABCDEF \
  --body 'Instagram caption' \
  --destination-options '{"instagram":{"placement":"post"}}' \
  --yes \
  --json
```

OpenPMM checks these settings during the create request. If a required setting
is missing, the CLI reports a non-retryable `422` validation error.

Attach one video to make a Facebook Reel. OpenPMM selects the `reel` placement
automatically. Do not send a `facebook` member in `--destination-options`.

For a Bluesky video, attach one video Asset to one body item:

```bash theme={null}
openpmm posts create \
  --workspace ws_01JABCDEF \
  --destination dest_01JABCDEF \
  --body 'Video update' \
  --media-item '0:ast_01JABCDEF' \
  --destination-options '{"bluesky":{"languages":["en"],"content_labels":[],"alt_text_by_asset_id":{}}}' \
  --yes \
  --json
```

Bluesky accepts one MP4 video without other media.

For YouTube, set `thumbnail_timestamp_ms` to select a frame from the video.
This example selects the frame at 1 second:

```bash theme={null}
openpmm posts create \
  --workspace ws_01JABCDEF \
  --destination dest_01JABCDEF \
  --headline 'Feature title' \
  --body 'Feature description' \
  --media media_reel_01JABCDEF \
  --destination-options '{"youtube":{"kind":"short","privacy_status":"private","category_id":"22","self_declared_made_for_kids":false,"contains_synthetic_media":false,"notify_subscribers":false,"thumbnail_timestamp_ms":1000}}' \
  --yes \
  --json
```

The command can return while OpenPMM or the provider processes the Post. Add
`--wait` for bounded CLI polling on an immediate publication.

Use `--when queue` to select the next Destination queue slot.
OpenPMM uses the Workspace time zone.

```bash theme={null}
openpmm posts create \
  --workspace ws_01JABCDEF \
  --destination dest_01JABCDEF \
  --when queue \
  --body 'Queued update' \
  --yes \
  --json
```

## Edit a Post

You can change the copy and publishing options on a draft or scheduled Post.
The CLI gets the current ETag before it changes the Post.

```bash theme={null}
openpmm posts update send_01JABCDEF \
  --workspace ws_01JABCDEF \
  --body 'Updated first post' \
  --body 'Updated second post'
```

Use `--etag` to change the version that you reviewed.

Use `--destination-options` with JSON to change the Destination publishing options.
The scheduled Post keeps its current time and assets.

<Warning>
  Cancel the schedule before you change assets or thread structure.
</Warning>

## Publish a Post

<Warning>
  Publishing can create external posts. Pass `--yes` to send the request.
</Warning>

```bash theme={null}
openpmm posts publish \
  --workspace ws_01JABCDEF \
  --post send_01JABCDEF \
  --post-version 2 \
  --destination dest_01JABCDEF \
  --yes \
  --wait \
  --json
```

Use `--at 2026-08-13T12:00:00Z` to schedule the Post at an exact time.
Use `--at queue` to select the next queue slot.
If a group contains multiple draft Posts, publish all Posts with `--file request.json`.

`posts publish` returns after OpenPMM accepts the state change. It does not
wait for provider completion unless you add `--wait`. Exit code `0` means that
OpenPMM accepted or completed the requested transition. Exit code `11` means
that one or more Posts need action. Both exits preserve the JSON result and
available receipts.

For immediate publication, inspect `meta.publication`:

* If `complete` is `true`, stop. The returned Posts include available receipts.
* If `complete` is `false`, use the returned `next_command`, or run
  `openpmm posts wait <post-id> --json`.
* If `action_required_post_ids` is not empty, stop and inspect each Post's
  `available_actions`. The CLI exits with code `11`. Do not keep polling.
* Read top-level `outcome`: `success`, `partial_failure`, or `failure`.
* Do not send `posts publish` again.

`--wait` polls through the CLI for up to 300 seconds by default. Set
`--wait-timeout` to change the bound. The final JSON includes `wait_complete`.
The CLI stops when each Post is terminal or needs action. Scheduled and queued
Posts are accepted work and do not use `--wait`.

## Move a queued Post

Get the Post and copy its current `scheduled_at` value.
Select a local date in the Workspace time zone.

```bash theme={null}
openpmm posts move-in-queue \
  --workspace ws_01JABCDEF \
  --post send_01JABCDEF \
  --expected-scheduled-at 2026-08-20T08:37:00.000Z \
  --local-date 2026-08-22 \
  --yes \
  --json
```

Use `--file request.json` to move multiple Posts in one atomic request.
OpenPMM moves all requested Posts or does not move any Post.

When background work continues, the command returns a nonterminal Post and a
next command in JSON output. Use `posts wait` for immediate publication. You
can use `posts show` for one read. OpenPMM continues the accepted work. Do not
run `posts publish` again.

## List Posts by state

Use `--view` to filter `posts list` by state:

```bash theme={null}
openpmm posts list --workspace ws_01JABCDEF --view failed --json
```

The views are `all`, `drafts`, `scheduled`, `published`, `attention`, and
`failed`. `failed` returns the Posts that need attention — the same set the
**Failed** tab shows in the web app.

The list can include Posts that OpenPMM imports from a connected provider.
Read `publication_source` in JSON output to identify them.
The value is `provider` for an imported Post.
The value is `openpmm` for a Post that OpenPMM publishes.

Use exact filters when you reconcile a workflow:

```bash theme={null}
openpmm posts list \
  --workspace ws_01JABCDEF \
  --group launch-2026-08 \
  --destination dest_01JABCDEF \
  --state needs-attention \
  --include attempts \
  --json
```

If `retry_safety` is `may_duplicate`, verify the provider first. Then add
`--acknowledge-duplicate-risk` to `posts retry`.

## Recover an uncertain request

The CLI safely retries a request that has an idempotency key.
Reuse `--idempotency-key` after a timeout.

If a provider stops after it publishes part of a thread, OpenPMM keeps the
published receipts. A retry resumes at the first missing reply instead of
publishing the opening posts again.
