Upload to Slack
Your app owns posting. Build the complete envelope before the first post attempt:
interface PreparedSlackPost {
readonly channel: string;
readonly threadTs?: string;
readonly blocks: ConversionResult["blocks"];
readonly text: string;
readonly receipts: ConversionResult["receipts"];
}
const result = await converter.convert(markdown, { timeoutMs: 30_000 });
const prepared: PreparedSlackPost = {
channel,
threadTs,
blocks: result.blocks,
text: result.text,
receipts: result.receipts,
};
await postPrepared(prepared, { timeoutMs: 10_000 });On post failure, return or throw this exact envelope and retry posting that.
Persist prepared -> posted only when your app needs crash recovery. That persistence
still does not make Slack posting idempotent. slackmark provides no outbox, worker,
database, or posting API.
You need three scopes. files:write covers the default external upload flow, files:read
is needed only with waitForProcessing: true, and chat:write is for your own posting.
FetchSlackUploader defaults:
| Option | Default |
|---|---|
| Per-stage request timeout | 10 seconds |
| Processing polling | off |
| Poll interval / attempts / total window | 250 ms / 20 / 30 seconds |
channelId | absent (private completion) |
| Retry / idempotency / orphan cleanup | none |
An upload moves through five stages: admission, upload-URL allocation, byte upload,
completion, and an optional file-info fetch. A rejection at admission carries phase
requested and retry safety safe, because the uploader was never invoked.
Receipts record phase, file ID, sharing, processing, status, Retry-After, provider code,
and retry safety. may-duplicate requires reconciliation; never means reuse the
completed result. A polling failure after completion returns
processing: "unverified" and never triggers replacement upload.
Whether to re-post a message, whether to re-upload a file (retrySafety), and whether to
re-run a conversion are three separate questions, each with its own signal.
Do not log receipts or source-bearing URLs broadly. Files complete without a channelId.
Confirm in your own workspace that a privately completed file renders for every viewer you
expect.