Cadence Connector
WordPress plugin: links posts into WPML translation groups from an external content pipeline, and refuses any request that disagrees with the site's own state.
by Elegia · github.com/elegia-io/cadence-wordpress · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/elegia-io/cadence-wordpress/releases/download/v0.1.0/cadence-connector.zipReadme
Cadence Connector for WordPress
Publishes content produced by Cadence into WordPress, and links the language variants of a piece into a single WPML translation group.
Why it exists
WPML has no documented REST endpoint for creating or linking translated
posts. WPML's own staff concede as much on their support forum. The sidestep
(POST /wp/v2/posts/{id}?lang=xx) tags a post's language but does not write the
translation-group relationship, so the language variants never become each
other's translations. Somebody publishing in four languages links three pairs by
hand for every piece.
This plugin exposes an endpoint that writes that relationship through WPML's own
wpml_set_element_language_details action.
Requirements
- WordPress 6.4+
- PHP 8.1+
- WPML with the String Translation and Translation Management add-ons, for the translation-linking endpoint. The publishing endpoint does not need WPML.
Installing
Download cadence-connector.zip from the
latest release.
In WordPress, go to Plugins → Add New → Upload Plugin, choose the zip, install
it and activate.
Both endpoints are then live under /wp-json/cadence/v1/. Nothing else is added:
no settings page, no post types, no front-end output.
Callers authenticate as a WordPress user. Application Passwords (Users → Profile → Application Passwords) are the usual choice for a pipeline, and the user needs the capabilities each endpoint asks for below.
The rule it is built around
Never write a wrong translation link.
A missing link costs a human one action in wp-admin. A wrong link tells the site
that the German post is the translation of the wrong Italian one, and the site
then serves it to visitors under a false hreflang.
Worse, WPML's documentation for wpml_set_element_language_details says: "If
set to FALSE it will create a new trid for the element causing any potential
translation relations to/from it to disappear." So writing with an unestablished
group does more than fail to link: it destroys relationships that already exist,
including ones a human made by hand.
Every refusal in this plugin therefore fails toward writing nothing. Any change that makes linking more eager is a regression even if it raises the automation rate.
This plugin also does not trust its caller. The caller computes a plan and has its own refusals; those run somewhere else, on someone else's machine. This code runs on your server, so it re-derives every precondition from the database before it writes, including reading each post's current translation group and refusing when the plan disagrees with what the site actually says.
The endpoints
There are two, and they do not overlap: one puts content on the site, the other says which pieces of content are translations of each other.
Publishing content
POST /wp-json/cadence/v1/content
Requires the post type's own create_posts capability, and publish_posts as
well when status is publish. Both are asked of the type, because WordPress
derives a type's capabilities from its registration and a custom type's are not
post's.
{
"external_id": "piece-2026-08-31-en",
"post_type": "post",
"status": "draft",
"title": "A title",
"content": "<p>Body.</p>"
}
external_id is what makes a retry safe. An HTTP pipeline retries, and a
request that timed out after WordPress committed the insert is
indistinguishable, to the caller, from one that never ran. Retried without an
identifier, it puts the same article on the site twice, published and visible to
visitors. So the identifier decides: one already on a post is answered with that
post, and nothing is created.
| Status | ||
|---|---|---|
201 |
created | created: true, with post_id |
200 |
it already existed | created: false, same post_id |
A repeat under an identifier that is already used is not an update. A different body under the same identifier means the caller believes it is publishing something new; the live article is not this plugin's to overwrite on that belief. Trashed posts still answer for their identifier, so a piece somebody deleted is not resurrected by the next run of the pipeline.
There is no update endpoint yet.
Linking translations
POST /wp-json/cadence/v1/translation-group
Requires edit_post on every post the request names, asked per post rather than
the blanket edit_posts, which a contributor holds.
{
"trid": null,
"create_group": true,
"source": {"post_id": 12, "language_code": "en", "element_type": "post_page",
"source_language_code": null},
"translations": [{"post_id": 34, "language_code": "de", "element_type": "post_page",
"source_language_code": "en"}]
}
Either create_group (make a new group from these posts) or trid (join this
existing one). Both together is refused rather than reconciled: it asks for two
different things and one of them destroys relations.
Every post is read before any post is written, so a request that is wrong about its last post writes nothing about its first.
Answers
| Status | Meaning | What the caller should do |
|---|---|---|
200 |
Written. written is how many. |
Nothing. |
400 |
The request is wrong on its face. | Fix it; re-sending cannot help. |
409 |
The site disagrees with the request. | Re-read the site and try again. |
503 |
The site has no WPML. | Install it; the request is fine. |
500 |
Refused for a reason this version cannot classify. | Report it. |
Refusals carry a stable code as well as a human reason. Match on the code;
the reason is prose and changes freely.
| Code | ||
|---|---|---|
bad_plan |
400 | the body is not the shape it claims |
contradictory_instructions |
400 | create_group and a trid together |
no_group_named |
400 | neither of them |
group_unknown |
409 | WPML returned nothing usable for a post, which is not "in no group" |
already_grouped |
409 | a post is already in a group, and creating one would detach it |
group_disagreement |
409 | the site's group for a post is not the one named |
wpml_unavailable |
503 | nothing on this site implements the WPML hooks |
bad_request |
400 | the content body is not the shape it claims |
insert_failed |
500 | WordPress refused the insert, or returned no id |
Development
./run-tests.sh runs the test suite in a container. PHP is not needed on your
machine, only podman or docker.
./run-tests.sh # everything
./run-tests.sh --filter PluginTest # one class
Licence
GPL-2.0-or-later. See LICENSE.
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v0.1.0 | Aug 31, 2026 | cadence-connector.zip | 1 |