@render-lab/tasks-strapi
v0.1.2
Published
Durable Strapi tasks for Render Workflows: strapi.find/findOne/create/update/delete/upload.
Readme
@render-lab/tasks-strapi
⚠️ Experimental: proof of concept. This package is part of the Render Tasks POC and is published for testing only. It is not fully tested or production ready. Task names, inputs, outputs, and behavior can change or break in any release. Pin exact versions and expect breaking changes.
Durable Strapi tasks for Render Workflows. Targets Strapi 5 (documents are identified by a stable documentId).
import {
find,
findOne,
create,
update,
deleteDocument,
upload,
findMedia,
deleteMedia,
} from "@render-lab/tasks-strapi";| Task | Input | Output |
| -------------------- | -------------------------------------------------------- | ------------------------- |
| strapi.find | { pluralApiId, query? } | { data[], meta } |
| strapi.findOne | { pluralApiId, documentId, query? } | { data \| null, meta } |
| strapi.create | { pluralApiId, data, query? } | { data, meta } |
| strapi.update | { pluralApiId, documentId, data, query? } | { data, meta } |
| strapi.delete | { pluralApiId, documentId } | { documentId, deleted } |
| strapi.upload | { base64, contentType, filename, ref? } | MediaFile[] |
| strapi.findMedia | { query? } | MediaFile[] |
| strapi.deleteMedia | { id } | MediaFile |
The
strapi.deletetask is exported asdeleteDocument/deleteDocumentImpl(deleteis a reserved word); its registered task name is stillstrapi.delete.
A StrapiDocument is an open JSON object carrying at least documentId (attributes are your content model). MediaFile is { id, name, url, mime, size, alternativeText, caption }. query is a StrapiQuery (filters, fields, sort, populate, pagination, status, locale) serialized qs-style — matching what the qs library produces for the Strapi REST API. List results are byte-bounded against the 4 MB task-result cap.
find/findOne read content; pass query.status (draft | published) to select the publication state (Strapi 5 exposes draft/published as a retrieval filter, not a REST publish action). create/update/delete write content by documentId. upload posts a file to the Media Library (optionally attaching it to an entry field via ref); findMedia/deleteMedia manage stored files.
Idempotency & retries
find, findOne, update (by documentId), and delete converge on replay and run under STRAPI_RETRY. create runs under STRAPI_NO_RETRY: Strapi assigns a fresh documentId, so a retried create after a lost response would duplicate (ADR-0018). upload similarly creates a new file id per call — dedupe downstream if a replay could double-upload.
Install
pnpm add @render-lab/tasks-strapi @renderinc/sdk@renderinc/sdk is a peer dependency; @render-lab/triggers is an optional peer (webhook adapter only). The default port talks to the Strapi REST API over the global fetch — no vendor SDK dependency.
Environment contract
| Variable | Required | Purpose |
| ----------------------- | -------- | -------------------------------------------------------------------------- |
| STRAPI_URL | yes | Base URL of your Strapi instance (e.g. https://cms.example.com). Read lazily. |
| STRAPI_API_TOKEN | yes | API token (Bearer) with the needed permissions. Read lazily at first call. |
| STRAPI_WEBHOOK_SECRET | no | Shared secret for the webhook adapter. Read lazily at verify time. |
A missing required var throws a clear error from the task that needs it (fail-close, ADR-0007). Any non-2xx response throws so the durable retry (STRAPI_RETRY) is the single source of truth (ADR-0005).
Webhooks
Strapi does not HMAC-sign webhooks; instead you configure static headers on the webhook (commonly Authorization). The adapter verifies that shared secret in constant time. It lives at a registration-free subpath (ADR-0017) and never calls task():
import { strapiAdapter } from "@render-lab/tasks-strapi/webhooks";
const adapter = strapiAdapter({
// secret defaults to STRAPI_WEBHOOK_SECRET; header defaults to "authorization" (Bearer)
onEvent: ({ event, payload }) =>
event === "entry.publish"
? { task: "content.index", args: [{ model: payload.model, entry: payload.entry }] }
: null,
});Set header (and bearer: false) to verify a custom header like x-webhook-secret instead.
Extending & testing
Every operation exports the wrapped task and its raw *Impl. The impls depend on a small StrapiPort interface, so they unit-test without touching the network:
import { updateImpl, type StrapiPort } from "@render-lab/tasks-strapi";
const strapi = {
update: async () => ({ data: { documentId: "abc123", title: "New" }, meta: {} }),
} as StrapiPort;
await updateImpl({ pluralApiId: "articles", documentId: "abc123", data: { title: "New" } }, { strapi });Run the tests with pnpm -C packages/tasks-strapi test.
