← All playbooks · Raw API
task-media-reuse-and-upload
# Capability: Media — reuse and upload
## Intent phrases
- upload image
- find existing asset
- add photo to page or article
- update person headshot / portrait
## Requires capabilities
`assetSearch` and `assetUpload`
## Prerequisites
- **Always run `index sync` first** if the catalog may be stale
- On **hosted MCP**, the server has **no filesystem** — use **staged upload** for all binary uploads (never `--base64` or a local file path in `args`)
## Reuse-first workflow (default)
1. `cms_edit ["index", "sync"]`
2. **Search existing assets:**
- `cms_edit ["asset", "search", "--filename-match", "<partial-name>"]`
- `cms_edit ["list", "--type", "media", "--asset-filename-match", "<partial>"]`
3. If match found — link by asset ID or filename in `set` / `create from-json` (`featuredImageAssetFilename`, `visualAssetFilename`, etc.)
4. For **`visuals`** (Media entry **array** on page/article): prefer an **existing Media entry** — `cms_edit ["list", "--type", "media", "--asset-id", "<assetId>"]` — then `set @root visuals <mediaEntryId> --links`. Do **not** `create media` or use `asset upload --with-media` when a wrapper already exists.
5. For **singular** component fields (e.g. `visual` on a component):
`cms_edit ["set", "@cN", "visual", "<mediaEntryId>", "--link"]`
**Never** `add media --parent @cN --target visual` — `add --target` only works for Entry-link **arrays** (`content`, `contents`, …). Bad `add --target` is rejected; it used to orphan Media drafts.
6. `create media` only when no Media entry wraps that asset (RTF embeds, or first article in a new composite batch — then **reuse that media ID** on sibling entries)
7. Bare Contentful entry IDs work with `set` (no `@` required) for mid-session Media field tweaks (width/autoplay/loop) after import — `set <mediaEntryId> width 75`.
## Upload only when no match
### Oversized masters / whole Drive folders
If files are near or over **4 MB** (hosted staged) or **10 MB** (URL/local), or the user shared a **Drive folder of masters**, do **not** improvise compression in chat.
1. Run **`task-media-pipeline-prepare`** (Drive folder → ready public URLs under limits)
2. Return here and import with **`--url`** + reuse-first filename search
### Hosted MCP (default)
**Always use staged upload** for ad-hoc small binaries. Do not pass base64 in `cms_edit` args — hosted MCP rejects `asset upload --base64`.
1. `cms_edit_request_staged_upload` with `fileName`, `mimeType`, optional `byteLength`, optional `title`
2. Run the returned `curlCommand` in a shell (POST file to `uploadUrl`)
3. `cms_edit` with the returned `consumeArgs` (e.g. `["asset","upload","--staged","<uploadId>",…]`)
Rules:
- Staged upload keeps binary **out of MCP args** — only a short `uploadId` is passed to Contentful
- Hosted limit: **4 MB** per file; **1000 upload URLs per user per hour**; TTL **60 minutes**; single-use
- **PNG** when the source has transparency (portraits on brand-colour cards); JPEG flattens alpha
- Upload alone does **not** attach the asset — follow with **link + save** (below)
- Use `--if-exists-by-filename` (with `--file-name`) to avoid duplicate assets when re-running
After staged upload, create a Media wrapper **only when none exists** for that asset:
```
cms_edit ["list", "--type", "media", "--asset-id", "<newPortraitAssetId>"]
cms_edit ["create", "media", "--asset-id", "<newPortraitAssetId>"]
```
**Media entry `name` (CMS list label):** defaults to the **linked asset title**. Only pass `--name` when you deliberately need a different label (e.g. two Media wrappers on one asset with distinct roles). Never use `visual-{assetId}` or `Wrapper for {assetId}` — those are migration leftovers.
To rename an existing Media wrapper: `cms_edit ["batch", "set", "<media-entry-id>:name=<asset-title>"]` then `batch save`.
Avoid `asset upload --with-media` when multiple entries will share the same image (e.g. news composites) — it creates duplicate Media entries. Upload the asset only, then one shared `create media`, then link the media ID on each entry.
**Prefer `--url`** when the file is already on **public HTTPS** under size limits — especially **media-pipeline ready pack URLs**:
```
cms_edit ["asset", "upload", "--url", "https://<public-https-url>/file.png", "--mime", "image/png", "--file-name", "headshot-name-1200.png", "--title", "…", "--if-exists-by-filename"]
```
The URL must be fetchable by the hosted MCP server. Do not use local-only URLs.
### Local CLI (only when user approves)
Local `cms-edit` may use `--base64` or `--url` when the user explicitly approves local CLI (see repo `Agents.md`). Hosted agents must not use local CLI without permission.
## Link uploaded asset to an entry
Upload returns an **asset ID**. Attach it, then save:
**Person portrait (`media` is an Asset link):**
```
cms_edit ["open", "--id", "<person-entry-id>"]
cms_edit ["asset", "set", "@root", "media", "<new-asset-id>"]
cms_edit ["diff"]
cms_edit ["save"]
```
**Component `visual` (Media Entry link, singular):**
```
cms_edit ["set", "@cN", "visual", "<media-entry-id>", "--link"]
```
**Article featured image / asset fields:** `asset set <ref> <field> <asset-id>` (or `set … --asset`) when the field is an Asset link.
Always `diff` → `save`. Verify with `preview urls` on the public path.
## Confirmation gates
1. Report search results before uploading
2. Confirm upload filename/title with user if ambiguous
3. After upload, confirm `asset info <id>` shows expected width/height/size before linking
## Out of scope
- Publish assets (draft upload; human publishes in Contentful UI if needed)
- Deleting orphan assets from failed uploads (human cleanup in Contentful)
## Related resources
- `task-media-pipeline-prepare`
- `cms-edit://customer/defaults` (featured image rules)
- `cms-edit://customer/people` (person `media` field, when present)