# Schedule and results

When posts go out, how to wait for the owner without polling, and how to report what happened: jobs, post links, the
live check and how the posts did. This page is complete on its own; drafting and the approval request are in
[draft and request approval](https://app.flaghand.com/docs/drafting.md).

A time on a draft is only a proposal. Nothing is scheduled or posted until the owner approves that exact snapshot with
their passkey, and only the owner can do that.

## Proposing a time

- `post_at` on a draft: ISO 8601 with `Z` or an offset (`2026-11-03T09:30:00-08:00`), at least 2 minutes and at most
  90 days ahead; a time without an offset is refused. Request it with `mode: "schedule"`. Without a time, `post_now`
  posts it as soon as the owner approves.
- **Weekly posting slots:** `setConnectionSlots` (`PUT …/connections/{id}/slots`, `drafts` scope) with
  `{"zone": "America/New_York", "slots": [{"day": 1, "time": "09:00"}, {"day": 3, "time": "14:30"}]}` (`day` 0 is
  Sunday; `time` is local `HH:MM`). Then a **new** draft in `createPost` or `createDraft` can send
  `"post_at": "next_slot"` with its `connection_id` to take that account's next open time.
  `getNextConnectionSlots` previews the next open times without reserving them.
- **Best times:** `getBestTimes` (`GET …/best-times?platform=x`, `read` scope) suggests the next good times, learned
  from the X account's own posts (LinkedIn: published research). For an X account with stale learning it makes billed
  X reads first, so MCP runs it with `callWriteOperation`.
- **Start or refresh X learning:** `refreshBestTimes` (`POST …/best-times/refresh`, `drafts` scope or owner session,
  active plan) exposes the same Insights refresh, including `full: true` to reread its history. CLI:
  `flaghand best-times refresh --full true --json`. Existing credit and automation checks remain; it starts billed
  learning reads, not a publishing job. Read-only tokens cannot refresh. [Settings for agents](https://app.flaghand.com/docs/agent-settings).
- **Still hot:** for an X account that learns, `getBestTimes` and `getNextConnectionSlots` return `still_hot`: each post
  of the account still gaining views faster than 3 in 4 of its posts did at that age, how it's doing against the
  account's median (`score`), `cools_at` (when it's down to a usual post's pace at 3 hours old, if it slows the way the
  account's posts usually do) and `first_time_after`, the first of the returned times after that. An open suggestion
  in `listSuggestions` carries the same `still_hot` when its proposed time comes before a hot post cools. It's a hint
  only: no time is moved, held or refused because of it, and approval works as always. `[]` means nothing is hot;
  `null` means the account has no stored reads (it doesn't learn, or isn't X).
- **Time zone:** best times and slots use the workspace's `time_zone` (`renameWorkspace`, `PATCH …/workspaces/{workspace}`
  with `{"time_zone": "Europe/London"}`, `drafts` scope). `post_at`, approvals and jobs are exact instants.

To move an approved post, change its draft and request approval again: the edit voids the old approval, and the owner
approves the new time. Say so before you do it.

## What approval starts

When the owner approves, `schedule` and `post_now` items each become a **job**; `approve` items only record the
decision. A job's `state` is one of:

| state | means |
|---|---|
| `scheduled` | waiting for its time |
| `waiting` | a hand-first thread, waiting for the owner to post post 1 |
| `posting` | being sent now |
| `processing` | the platform is still processing it (TikTok, or a LinkedIn video or document) |
| `posted` | live; `result` has the links and the live check |
| `inbox` | TikTok put it in the creator's TikTok inbox as a draft: not published |
| `held` | stopped before sending, approval intact: `x_credits_exhausted`, `connection_login_expired`, `posting_paused` (the operator's pause under the Terms) or a platform refusing the app (`x_app_restricted`, `linkedin_app_restricted`, `tiktok_app_restricted`). The owner retries in the desk once it's fixed or lifted |
| `failed` | not posted; `error_code` and `error` say why |
| `canceled` | canceled before posting |

A job posts at most 2 hours late; past that it fails rather than post something stale. A job whose time comes while
the workspace's plan isn't active fails with `workspace_inactive`, and nothing is posted.

A platform's HTTP 429 refusal while preparing or sending a post ends the job as `failed` with
`platform_rate_limited`, rather than silently retrying at a later time. The error includes a reset hint when the
platform supplies one; it isn't a promise of capacity. The owner chooses a new time through a new approval request.
Any already-live X thread parts stay live and are reused, not posted twice. The existing failure notifications apply.
A 429 during X live read-back does **not** fail or resend the successful post: `result.check` explains that the check
is delayed. Once TikTok has accepted a `publish_id`, a status-query refusal keeps the job `processing` and never
starts a second publication. If TikTok rate-limits the separate video-byte upload after issuing its publish id, the
upload stops and the failure asks the owner to check TikTok and its inbox before approving again.

## Waiting without polling

Don't call a tool every minute in the conversation. In order of preference:

1. **A signed webhook**, if you have an HTTPS receiver and the `webhooks` scope (the owner adds it):
   `createWebhook` (`POST …/webhooks`) with
   `{"endpoint_url": "https://your-agent.example/events", "actions": ["approval.created", "job.posted", "job.failed", "job.held"]}`.
   Register it **before** requesting approval: it receives only future events. Its signing secret is in that answer
   only; keep it in the receiver's secret store. Verifying signatures: the
   [agents guide](https://app.flaghand.com/docs/agents.md#signed-webhooks).
2. **The CLI**, where it's installed: `flaghand drafts wait drf_…` waits quietly and answers once the draft is
   approved, posted, failed or sent back.
3. **Otherwise**, when the person comes back: `getDraft` for each draft, or `listEvents` (`GET …/events`) with
   `after` (start at `0`) and `draft_id`, following `next_after` and the `Retry-After` hint between turns.

Never claim a subscription or background wait exists when it doesn't. A sent-back draft shows
`status: "changes_requested"` with `changes_requested_note` (event `draft.changes_requested`): edit, check and request
approval again.

## Reporting the result

Find the job with `getDraft` (its `job`) or `listJobs` with `draft_id`, then `getJob` (`GET …/jobs/{id}`). Report only
what the job says.

- `posted`: give the person the post links, `result.urls`. For X, also `result.check`: Flaghand read the posts back
  from X and compared text, hidden reply mentions, media, alt text, the reply chain and the quote with what was
  approved. `verifyJob` reads it back again.
- LinkedIn posts can't be read back with the app's current permissions: `result.check.available` is `false`
  (`linkedin_readback_unavailable`). Report the link, never that the live post matches.
- TikTok: `inbox` means it's in the creator's TikTok inbox, not published; `processing` means TikTok is still working
  on it. `checkJobStatus` asks TikTok again.
- `failed` or `held`: report `error_code` and `error` as they are. Never say it posted. `duplicate_post` means the
  platform refused repeated text; a new draft with new wording needs a new approval.

## Cancel or correct

- `cancelJob` (`POST …/jobs/{id}/cancel`, `cancel` scope) stops a waiting, scheduled, held or failed job. It never
  deletes a live post; taking a live post down is the owner's, in the desk.
- A posted LinkedIn post's text can be corrected: `requestLiveEdit` (`PUT …/jobs/{id}/edit-request`, `drafts` and
  `request-approval`) with `{"text": "…", "note": "why"}` answers a `review_url` for the owner, who approves the edit
  with their passkey. X posts can't be edited.

## How the posts did

From the hourly reads Flaghand already makes for an X account that learns best times (8 days per post), so these
read nothing new from X and cost nothing:

- `listJobResults` (`GET …/job-results?posted_after=2026-10-19T00:00:00Z`): each posted X job's latest views, likes,
  reposts, replies, quotes and bookmarks.
- `listJobMetrics` (`GET …/jobs/{id}/metrics`): one job's records.
- `listTopPosts` (`GET …/top-posts?days=7&by=replies&limit=5`): the best posts of the last days by `views`, `likes` or
  `replies`. Learn from openings, length and topics; don't copy a post or quote its numbers as results.

An account that doesn't learn answers `not_measured` with nulls, never zeros.

### Score against the owner's median

Every posted X job's `x.score` (in `listJobResults`, `listJobMetrics`, `getMonthlyReport` and `getWeeklyReport`, as
`score` in `listTopPosts`, and as `posted.score` on a posted suggestion in `listSuggestions`) compares post 1 with the
same account's own posts **at the same age**, since views keep climbing for days:

- A post last read at 3 hours old is compared with what the account's other posts had at 3 hours, from their stored
  hourly reads: linear between the two reads around that age (at most 2 hours apart, or a third of the age if longer),
  from 0 at posting, never extrapolated past a post's last read.
- The usual is the **median** of the 30 original posts nearest in posting time that have a read at that age, so the
  account's growth or slump isn't counted for or against a post (Best times uses the same running median).
- From 7 days old, when views have nearly stopped, it compares the latest numbers with other posts' latest numbers
  read at 7 days or later. That also covers the history learning read back before its hourly reads began.
- Fewer than 5 comparable posts: `status: "too_few_posts"`, with no median or ratio rather than a noisy one.

`ratio` is views over the median views (2 is twice the usual at that age); `label` is `above` (better than 3 in 4 of
those posts), `below` (worse than 3 in 4) or `usual`. `metrics` gives views, engagements, likes and replies each with
its `value`, `median`, `ratio`, `percentile` and how many `posts` it was compared with; likes and replies at the same
age come from reads since late October 2026, so older posts may give fewer of them. `method` is `same_age_median`, and
`note` says the comparison in one sentence. A post under 7 days old is still read hourly, so its score can change.
An account that doesn't learn, or a post with no stored numbers, answers `not_measured` with nulls, never zeros.
`listTopPosts?by=score` ranks by the ratio, so a new post isn't ranked below older ones only for being new.

## Weekly results

`getWeeklyReport` (`GET …/weekly-report`, `read` scope) covers the 7 days before `until` (now when left out): each
posted X job with its text, numbers and score, `worked` and `flopped` (the job ids that scored above and below, best
and worst first), and the suggestions created, decided or posted that week: which were picked, edited before the
passkey approval, dismissed by a person or withdrawn by an agent, and how the picked ones did (`summary.picked_posted`
beside `summary.other_posts`). It reads stored data only, makes no X reads or model calls, and **sends nothing**: no
email, push or message. It's there when the person or their agent asks.

```sh
flaghand reports get-weekly --json
flaghand reports get-weekly --format markdown --out week.md
flaghand reports get-weekly --until 2026-10-26T00:00:00Z --format csv --out week.csv
```

In MCP, use `getOperation` for `getWeeklyReport`, then `callReadOperation` with
`{"operation_id":"getWeeklyReport","arguments":{}}`; add `"format":"markdown"` or `"format":"csv"` inside `arguments`
for text. How a post did is the audience's response, not the owner's preference: the `learn_voice` recipe reads this
report and proposes lessons only when several posted suggestions agree. Flaghand never makes a lesson from scores
by itself.

## Monthly results

For a client update or a vault note, `getMonthlyReport` (`GET …/reports/{month}`, `read` scope) combines published jobs
and links across networks, the X numbers already stored, approvals, send-backs and each agent's draft counts. In the
desk, open **Reports**, choose the workspace and UTC month, then **Copy Markdown** or **Download CSV**.

```sh
flaghand reports get-monthly 2026-10 --json
flaghand reports get-monthly 2026-10 --format markdown --out results-2026-10.md
flaghand reports get-monthly 2026-10 --format csv --out results-2026-10.csv
```

In MCP, use `getOperation` for `getMonthlyReport`, then `callReadOperation` with
`{"operation_id":"getMonthlyReport","arguments":{"month":"2026-10"}}`; add `"format":"markdown"` or
`"format":"csv"` inside `arguments` for text. This reads stored data only, makes no provider reads or refreshes, and
works in any plan state. The month is UTC, independent of the workspace's display time zone.

Read the returned `notes`: only completed posted jobs count (including recorded by-hand posts), not partial threads,
waiting jobs or TikTok inbox drafts. Posting times are completion/recording times. X numbers are the latest stored
reads, **not month-end snapshots**; missing numbers are null/empty, not zero. LinkedIn has links and times but **no post
numbers**, because `r_member_social` isn't granted. Decisions count each passkey approval and audit-log send-back made
that month, including repeats; imported local approvals are excluded. Agent counts instead use drafts **created that
month**, with outcomes as of generation, not only monthly events. CSV is rectangular: filter its `section` before
aggregating, so job totals and post rows aren't double-counted.
[Full counting and download details](https://app.flaghand.com/docs/agents.md#monthly-results).

## Operations on this page

The operation id is also the MCP tool's name; MCP says how to call it (`listed`: a default tool).

| Operation | HTTP | CLI | MCP | Scopes | Does |
| --- | --- | --- | --- | --- | --- |
| [`setConnectionSlots`](https://app.flaghand.com/docs/api/setConnectionSlots.md) | `PUT /workspaces/{workspace}/connections/{id}/slots` | `flaghand connections set-slots` | `callWriteOperation` | `drafts` | Replace the weekly proposed times; never moves a draft, approval or job |
| [`getNextConnectionSlots`](https://app.flaghand.com/docs/api/getNextConnectionSlots.md) | `GET /workspaces/{workspace}/connections/{id}/slots/next` | `flaghand connections get-next-slots` | `callReadOperation` | `read` | Preview the next open exact times; does not reserve or schedule them |
| [`getBestTimes`](https://app.flaghand.com/docs/api/getBestTimes.md) | `GET /workspaces/{workspace}/best-times` | `flaghand best-times get` | `callWriteOperation` | `read` | The next best times to post, learned from the account's X history (LinkedIn: published research) |
| [`renameWorkspace`](https://app.flaghand.com/docs/api/renameWorkspace.md) | `PATCH /workspaces/{workspace}` | `flaghand workspaces rename` | `callWriteOperation` | `drafts` | Update a workspace's time zone (drafts scope), or its name (owner session only) |
| [`createWebhook`](https://app.flaghand.com/docs/api/createWebhook.md) | `POST /workspaces/{workspace}/webhooks` | `flaghand webhooks create` | `callWriteOperation` | `webhooks` | Register a signed JSON receiver or Slack/Discord incoming-webhook alerts |
| [`listEvents`](https://app.flaghand.com/docs/api/listEvents.md) | `GET /workspaces/{workspace}/events` | `flaghand events list` | `callReadOperation` | `read` | The audit log: who did what; after follows new events in order |
| [`getDraft`](https://app.flaghand.com/docs/api/getDraft.md) | `GET /workspaces/{workspace}/drafts/{id}` | `flaghand drafts get` | listed | `read` | One draft, with its current approval and latest job |
| [`listJobs`](https://app.flaghand.com/docs/api/listJobs.md) | `GET /workspaces/{workspace}/jobs` | `flaghand jobs list` | listed | `read` | Jobs, newest first |
| [`getJob`](https://app.flaghand.com/docs/api/getJob.md) | `GET /workspaces/{workspace}/jobs/{id}` | `flaghand jobs get` | `callReadOperation` | `read` | One job |
| [`verifyJob`](https://app.flaghand.com/docs/api/verifyJob.md) | `POST /workspaces/{workspace}/jobs/{id}/verify` | `flaghand jobs verify` | `callWriteOperation` | `read` | Read a posted X job or paused thread's live prefix back and compare it with the approval |
| [`checkJobStatus`](https://app.flaghand.com/docs/api/checkJobStatus.md) | `POST /workspaces/{workspace}/jobs/{id}/status` | `flaghand jobs check-status` | `callWriteOperation` | `read` | TikTok: read the attempt's status from TikTok again (never sends it again) |
| [`cancelJob`](https://app.flaghand.com/docs/api/cancelJob.md) | `POST /workspaces/{workspace}/jobs/{id}/cancel` | `flaghand jobs cancel` | `callWriteOperation` | `cancel` | Cancel a waiting, scheduled, held or failed job, a processing LinkedIn video, or a posted job's plug or repost that hasn't gone out (the original stays live) |
| [`requestLiveEdit`](https://app.flaghand.com/docs/api/requestLiveEdit.md) | `PUT /workspaces/{workspace}/jobs/{id}/edit-request` | `flaghand jobs request-live-edit` | `callWriteOperation` | `drafts` and `request-approval` | LinkedIn: ask the owner to approve new text for a posted post; nothing changes on LinkedIn until they approve it with their passkey |
| [`listJobResults`](https://app.flaghand.com/docs/api/listJobResults.md) | `GET /workspaces/{workspace}/job-results` | `flaghand jobs list-results` | `callReadOperation` | `read` | How posted X jobs did: each post's latest views, likes, reposts, replies, quotes and bookmarks, newest first |
| [`listJobMetrics`](https://app.flaghand.com/docs/api/listJobMetrics.md) | `GET /workspaces/{workspace}/jobs/{id}/metrics` | `flaghand jobs list-metrics` | `callReadOperation` | `read` | How a posted job performed: every record, newest first, with rates, and for X its numbers from X |
| [`listTopPosts`](https://app.flaghand.com/docs/api/listTopPosts.md) | `GET /workspaces/{workspace}/top-posts` | `flaghand jobs list-top-posts` | `callReadOperation` | `read` | The workspace's best X posts of the last days, ranked by views, likes or replies, with their text |
| [`getWeeklyReport`](https://app.flaghand.com/docs/api/getWeeklyReport.md) | `GET /workspaces/{workspace}/weekly-report` | `flaghand reports get-weekly` | `callReadOperation` | `read` | The last 7 days on X: each post against the account's median at the same age, what worked and what flopped, and the suggestions picked, edited or dismissed |
| [`getMonthlyReport`](https://app.flaghand.com/docs/api/getMonthlyReport.md) | `GET /workspaces/{workspace}/reports/{month}` | `flaghand reports get-monthly` | `callReadOperation` | `read` | Monthly results: published posts, stored X numbers, approvals, send-backs and each agent's drafts |

Also complete on their own: [set up and connect](https://app.flaghand.com/docs/setup.md),
[draft and request approval](https://app.flaghand.com/docs/drafting.md), [media and threads](https://app.flaghand.com/docs/media-and-threads.md),
[billing](https://app.flaghand.com/docs/billing.md) and [errors](https://app.flaghand.com/docs/errors.md).
