Manage Framer CMS content programmatically via the framer-api npm package. Push articles, upload images, create collections, and publish/deploy — all from the terminal, no Framer app needed.
If this is the first time the user uses this skill in a project, run the onboarding flow described in references/onboarding.md.
Quick check: Look for FRAMER_PROJECT_URL and FRAMER_API_KEY in the user's .env file or environment. If missing, onboard.
This skill uses the Framer Server API ( framer-api npm package) which connects to Framer projects via WebSocket using an API key. It provides full CMS CRUD, image uploads, publishing, and deployment.
Important: The framer-api package must be installed in the project. If not present, run:
bash
npm i framer-api
All operations use ES module scripts ( .mjs files) with this connection pattern:
javascript
import { connect } from "framer-api"
// IMPORTANT: API key is passed as a plain string (2nd argument), NOT as {apiKey: "..."}
const framer = await connect(process.env.FRAMER_PROJECT_URL, process.env.FRAMER_API_KEY)
try {
// ... operations ...
} finally {
await framer.disconnect()
}
OperationMethodNotesList collectionsframer.getCollections()Returns all CMS collectionsGet one collectionframer.getCollection(id)By collection IDCreate collectionframer.createCollection(name)Creates empty collectionGet fieldscollection.getFields()Field definitions (name, type, id)Add fieldscollection.addFields([{type, name}])Add new fields to collectionRemove fieldscollection.removeFields([fieldId])Delete fields by IDReorder fieldscollection.setFieldOrder([fieldIds])Set field display order
OperationMethodNotesList itemscollection.getItems()All items with field dataCreate itemscollection.addItems([{slug, fieldData}])Create new items. Returns undefined — re-fetch with getItems() to get IDsUpdate item fieldsitem.setAttributes({ fieldData: { [fieldId]: {type, value} } })MUST wrap in fieldData: — without it, values are silently ignoredUpdate item slug/draftitem.setAttributes({ slug: "new", draft: false })Slug and draft are set directly (NOT inside fieldData)Delete itemitem.remove()Single itemBulk deletecollection.removeItems([itemIds])Multiple itemsReorder itemscollection.setItemOrder([itemIds])Set display order
The setAttributes method has a non-obvious API design — field values MUST be wrapped in a fieldData key:
javascript
// ✅ CORRECT — fields wrapped in fieldData
await item.setAttributes({
fieldData: {
[titleFieldId]: { type: "string", value: "New Title" }
}
})
// ❌ WRONG — silently ignored, no error thrown
await item.setAttributes({
[titleFieldId]: { type: "string", value: "New Title" }
})
// ❌ WRONG — also silently ignored
await item.setAttributes({
[titleFieldId]: "New Title"
})
Partial updates work: Only specified fields are changed. Other fields are preserved.
Non-field attributes (slug, draft) go directly on the object, NOT inside fieldData:
javascript
await item.setAttributes({ slug: "new-slug", draft: false })
When creating/updating items, field data is keyed by field ID (not name):
javascript
const fields = await collection.getFields()
const titleField = fields.find(f => f.name === "Title")
await collection.addItems([{
slug: "my-article",
fieldData: {
[titleField.id]: { type: "string", value: "My Article Title" },
}
}])
Supported field types and their value format:
TypeValue formatExamplestring``string``{ type: "string", value: "Hello" }``number``number``{ type: "number", value: 42 }``boolean``boolean``{ type: "boolean", value: true }``date``string (UTC ISO){ type: "date", value: "2026-04-06T00:00:00Z" }``formattedText``string (HTML){ type: "formattedText", value: "<h2>Title</h2><p>Text</p>" }``link``string (URL){ type: "link", value: "https://example.com" }``image``ImageAsset objectSee image upload sectionenum``string (case name){ type: "enum", value: "Published" }``color``string (hex/rgba){ type: "color", value: "#FF0000" }``file``FileAsset objectSimilar to imagecollectionReference``string (item ID){ type: "collectionReference", value: "itemId123" }``multiCollectionReference``string[]``{ type: "multiCollectionReference", value: ["id1","id2"] }
Upload images from public URLs, then use the returned asset in CMS items:
javascript
const asset = await framer.uploadImage("https://example.com/photo.jpg")
// asset = { id, url, thumbnailUrl }
await item.setAttributes({
fieldData: {
[thumbnailField.id]: { type: "image", value: asset.url }
}
})
javascript
// Create a preview deployment
const result = await framer.publish()
// result = { deployment: { id }, hostnames: [...] }
// Promote preview to production
await framer.deploy(result.deployment.id)
Always ask the user before deploying to production. Publishing a preview is safe; deploying is live.
javascript
await framer.getProjectInfo() // { id, name, apiVersion1Id }
await framer.getCurrentUser() // { id, name, avatar }
await framer.getPublishInfo() // Current deployment status
await framer.getChangedPaths() // { added, removed, modified }
await framer.getChangeContributors() // Contributor UUIDs
await framer.getDeployments() // All deployment history
OperationMethodNotesColor stylesgetColorStyles(), createColorStyle()Design tokensText stylesgetTextStyles(), createTextStyle()Typography tokensCode filesgetCodeFiles(), createCodeFile(name, code)Custom code overridesCustom codegetCustomCode()Head/body code injectionFontsgetFonts()Project fontsLocalesgetLocales(), getDefaultLocale()i18nPagescreateWebPage(path), removeNode(id)Page managementScreenshotsscreenshot(nodeId, options)PNG buffer of any nodeRedirectsaddRedirects([{from, to}])Requires paid planNode treegetNode(id), getChildren(id), getParent(id)DOM traversal
See references/cms-operations.md for the full pattern including field resolution, image upload, and error handling.
javascript
const items = await collection.getItems()
for (const item of items) {
await item.setAttributes({
fieldData: {
[metaField.id]: { type: "string", value: generateMeta(item) }
}
})
}
javascript
const changes = await framer.getChangedPaths()
if (changes.added.length || changes.modified.length || changes.removed.length) {
const result = await framer.publish()
console.log("Preview:", result.hostnames)
// Ask user before: await framer.deploy(result.deployment.id)
}
connect() call opens a persistent WebSocket. Always call disconnect() when done, or use using framer = await connect(...) for auto-cleanup.getFields() first and resolve names to IDs.framerusercontent.com URL from uploadImage(), not the asset ID.Object.keys(framer) but work correctly.formattedText fields: Accept standard HTML (h1-h6, p, ul, ol, li, a, strong, em, img, blockquote, pre, code, table, etc.).draft: true — drafts are excluded from publishing."thisPlugin" are read-only via the API. Only "user" managed collections can be modified.