What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Migrating to Agility
Export posts, pages, media, categories, tags and ACF fields with the WordPress REST API, map them to Agility models, lists and pages, import them with the Management SDK, and redirect old permalinks.
Last verified: 2026-10-03 against the WordPress REST API Handbook and the Advanced Custom Fields (ACF) documentation. WordPress sites vary with their plugins and themes, so check your own site's endpoints before you rely on a detail here.
This guide applies the phases in Migrating to Agility to a WordPress site: export with the WordPress REST API, map WordPress content to Agility models, lists and pages, import with the Management SDK, and redirect old permalinks. For the same migration driven by an AI agent through the Agility MCP Server, see Migrating Content into Agility with an AI Agent, which also uses WordPress as its example.
Every WordPress site exposes its own REST API, under /wp-json/ on the site's domain. The routes you need for a content migration:
| Content | Route | Useful fields |
|---|---|---|
| Posts | /wp-json/wp/v2/posts | id, title, content, excerpt, slug, date_gmt, modified_gmt, author, featured_media, categories, tags, link, meta |
| Pages | /wp-json/wp/v2/pages | The post fields, plus parent (the parent page's ID) and menu_order |
| Media | /wp-json/wp/v2/media | source_url, alt_text, caption, mime_type, media_details, post |
| Categories | /wp-json/wp/v2/categories | id, name, slug, parent, description, link, count |
| Tags | /wp-json/wp/v2/tags | id, name, slug, link |
| Users (authors) | /wp-json/wp/v2/users | The public profile fields |
| Post types and taxonomies | /wp-json/wp/v2/types, /wp-json/wp/v2/taxonomies | Lists what else the site exposes |
Custom post types and custom taxonomies appear in the API only if they were registered with show_in_rest => true. Check /wp-json/wp/v2/types for the list. A type that isn't there needs that flag set in WordPress, or another export route.
ACF fields appear under an acf key on posts, pages, users and terms, but only for field groups that have Show in REST API turned on in their group settings. This support was added in ACF 5.11. ACF returns light formatting by default; pass acf_format=standard if you want fully formatted values, such as complete image data.
per_page defaults to 10 and is capped at 100. Loop on page, and use the X-WP-TotalPages response header to know when to stop. X-WP-Total gives the total count, which you will need for verification.status=publish). To export drafts, scheduled or private items, request them with the status parameter as an authenticated user, for example with an Application Password (available since WordPress 5.6)._fields=id,slug,title,link returns only the fields you list, which makes inventory calls fast. _embed includes linked resources such as the author, terms and featured image in one response.modified_after returns only items changed since a date. You will use it for the final sync at cutover.// Read every item from a WordPress collection, 100 at a time.
async function fetchAll(site: string, route: string, params = "") {
const items: any[] = []
for (let page = 1; ; page++) {
const res = await fetch(`${site}/wp-json/wp/v2/${route}?per_page=100&page=${page}${params}`)
if (!res.ok) break
items.push(...(await res.json()))
if (page >= Number(res.headers.get("X-WP-TotalPages"))) break
}
return items
}
const posts = await fetchAll("https://example.com", "posts")
const categories = await fetchAll("https://example.com", "categories")
content.rendered is the HTML WordPress produces for the post. The block editor stores each block in the post content as HTML wrapped in comment delimiters, with the block's settings as JSON inside the comment, for example <!-- wp:image {"sizeSlug":"large"} -->. If you only need the body as HTML, the rendered content is enough. If you want to turn particular blocks into Agility fields or components, plan how you will get at the stored block markup for your site before you design the mapping.
Also look for leftovers that don't make sense outside WordPress: unrendered shortcodes in square brackets, page-builder wrapper markup, and image URLs under /wp-content/uploads/.
| WordPress | Agility | Notes |
|---|---|---|
| Posts | A content model (for example BlogPost) with a list container, plus a dynamic page that gives each post its own URL | See Generating Dynamic Pages From Content. Use the slug as the path. |
| Pages | Pages in the sitemap, built from a page model and components | parent becomes the page's place in the sitemap tree. For many similar pages, a list plus a dynamic page can be simpler. |
| Custom post types | One content model and list per type | |
| Categories | A Category content model and list | Posts point at categories with linked content: a dropdown for one category, a search list box for several. Keep parent as a linked field or flatten it. |
| Tags | A Tag content model and list | Linked from posts with a search list box. |
| Users | An Author content model and list | Import only the public fields your site shows. |
| Media library | The Agility media library | Upload files, then reference the returned cdn.aglty.io URL. |
| Featured image | An Image field | Look up featured_media in the media export. |
content.rendered | An HTML field, a Markdown field, or fields and components | See the section above. |
excerpt.rendered | A Multi-Line Text field | The rendered excerpt is HTML, so strip the tags. |
date_gmt | A Date/Time field | The GMT value avoids time-zone ambiguity. |
slug | A Text field used in the dynamic page's path | Keeping slugs keeps URLs stable. |
id | A SourceID Text field | Makes re-runs update items instead of duplicating them. |
| ACF field type | Agility field type |
|---|---|
| Text, Email | Text |
| Text Area | Multi-Line Text |
| WYSIWYG Editor | HTML |
| Number, Range | Number, or Decimal for fractional values |
| True / False | True/False |
| Select, Radio Button, Button Group | Drop-down List |
| Checkbox | Linked content to a small list, or a Text field holding the values |
| URL, Link | URL |
| Image | Image |
| File | File |
| Gallery | Gallery |
| Date Picker, Date Time Picker | Date/Time |
| Post Object, Relationship, Page Link | Linked Content |
| Taxonomy, User | Linked Content to your Category, Tag or Author list |
| Group | The group's fields directly on the model, optionally under a Tab |
| Repeater, Flexible Content | Nested linked content (a list of child items) or components on a page |
The field types are described in Fields and Linked Content Field Types. Remember that a linked-content field stores its selection in two companion fields, _ValueField (the linked item's content ID) and _TextField. Read back an item you set by hand in the editor and copy that shape.
Import in this order, keeping a map from WordPress ID to Agility content ID (or asset URL) at each step:
assetMethods.upload, keyed by WordPress media ID.Before saving, rewrite the body HTML: point image src attributes at the uploaded cdn.aglty.io URLs, and change internal links from old permalinks to the new paths so readers don't go through a redirect.
// Map one WordPress post to an Agility content item.
// existing: SourceID -> contentID map built by paging through the container first.
function toAgilityItem(post: any, existing: Map<string, number>, categoryIds: Map<number, number>, imageUrls: Map<number, string>) {
const categoryId = categoryIds.get(post.categories[0])
return {
contentID: existing.get(String(post.id)) ?? -1, // update if it exists
properties: {definitionName: "BlogPost", referenceName: "BlogPosts"},
fields: {
Title: post.title.rendered,
Slug: post.slug,
Body: rewriteLinks(post.content.rendered), // your link and image rewriting
PublishDate: post.date_gmt,
SourceID: String(post.id),
Category_ValueField: categoryId ? String(categoryId) : "",
// Set Category and Category_TextField, and the image field, using the
// exact shapes you read back from a sample item.
},
}
}
Save with contentMethods.saveContentItems in sequential batches, never in parallel, and check the result for -1 entries (failed items). The full script, including the lookup of existing items, asset upload and batch publishing, is in Migrating Content into Agility with an AI Agent. Throughput guidance is in Migrating to Agility.
For pages: if there are only a handful, build them by hand in the sitemap. For many, use the SDK's page methods (see Pages).
The Agility CLI doesn't read WordPress. It is useful after the import, to copy the finished models and content from a migration sandbox instance into a blank production instance.
Every WordPress post, page and term has a link field with its public URL. Use it to build the URL map:
link for posts, pages, categories and tags (and any custom post types).Load the redirects into Agility's URL Redirections list. The import file has three columns, OriginURL, DestinationURL and HTTPCode (301 or 302); see URL Redirections. From code, the Management API's POST /api/v1/instance/{guid}/url-redirections creates or updates up to 250 redirects per call and reports any it skipped, for example an origin URL that already exists. Your front end must apply the redirects; see Implementing URL Redirects with Next.js.
The SEO side of this, including metadata and sitemaps, is in Keep Your Search Rankings Through a Migration.
/wp-json/wp/v2/types and /wp-json/wp/v2/taxonomies.acf appears in the responses.X-WP-Total for each type (minus anything skipped on purpose)./wp-content/uploads/ URLs.