The @webiny/sdk package provides a TypeScript interface for external apps (Next.js, Vue, Node.js) to interact with Webiny's Headless CMS and File Manager. Every method returns a Result object (checked with isOk()). Supports listing, getting, creating, updating, publishing, and unpublishing entries with filtering, sorting, pagination, and TypeScript generics for type safety.
npm install @webiny/sdk
Initialize once and reuse:
// lib/webiny.ts
import { Webiny } from "@webiny/sdk";
export const webiny = new Webiny({
token: process.env.WEBINY_API_TOKEN!,
endpoint: process.env.WEBINY_API_ENDPOINT!,
tenant: process.env.WEBINY_API_TENANT || "root"
});
token -- API key token generated in Webiny Admin (Settings > API Keys)endpoint -- The base API URL, without a trailing slash. Run yarn webiny info in your Webiny project to find the API URL. For Website Builder projects use NEXT_PUBLIC_WEBSITE_BUILDER_API_HOST.tenant -- Tenant ID, defaults to "root"
IMPORTANT: Never add a trailing slash to
endpoint. The SDK appends/graphqlto the endpoint internally, sohttps://xxx.cloudfront.net/will break all requests.
fields Parameter (Required)Every SDK method requires a fields array that specifies which fields to return. Omitting it will cause a runtime error.
"values.<fieldId>" for content fields: "values.name", "values.price"
"id", "entryId", "createdOn", "status"
"values.author.name"
// Minimal fields -- just IDs
fields: ["id", "entryId"];
// Content fields
fields: ["id", "entryId", "values.name", "values.price", "values.description"];
// Nested reference fields
fields: ["id", "values.title", "values.author.name", "values.author.email"];
webiny.cms.listEntries and webiny.cms.getEntry accept a preview parameter to control which revisions are returned:
preview |
Returns | Use For |
|---|---|---|
false (default) |
Published entries only | Public-facing apps, SSG |
true |
Latest revision (draft or published) | Content preview, editorial tools |
Write operations (createEntry, updateEntryRevision, etc.) are not affected by preview.
Every SDK method returns a Result object -- it never throws:
const result = await webiny.cms.listEntries({
modelId: "product",
fields: ["id", "values.name"]
});
if (result.isOk()) {
console.log(result.value.data); // success -- typed data
} else {
console.error(result.error.message); // failure -- error info
}
Pass a type parameter for full type safety on values:
import type { CmsEntryData } from "@webiny/sdk";
interface Product {
name: string;
price: number;
sku: string;
description: string;
category?: CmsEntryData<ProductCategory>;
}
interface ProductCategory {
name: string;
slug: string;
}
const result = await webiny.cms.listEntries<Product>({
modelId: "product",
fields: ["id", "entryId", "values.name", "values.price", "values.sku"]
});
if (result.isOk()) {
// result.value.data is CmsEntryData<Product>[]
const products = result.value.data;
// products[0].values.name -- fully typed
}
Reference fields like category are typed as CmsEntryData<T>, which wraps referenced entries with id, entryId, and values.
const result = await webiny.cms.listEntries<Product>({
modelId: "product",
fields: ["id", "entryId", "values.name", "values.price"],
sort: { "values.name": "asc" },
limit: 10
});
const result = await webiny.cms.listEntries<Product>({
modelId: "product",
fields: ["id", "entryId", "values.name", "values.price"],
where: {
"values.price_gte": 100,
"values.name_contains": "Pro"
},
sort: { "values.price": "desc" }
});
| Operator | Description | Example |
|---|---|---|
_eq |
Equals (default) | "values.status": "active" |
_not |
Not equals | "values.status_not": "archived" |
_contains |
Contains substring | "values.name_contains": "Pro" |
_startsWith |
Starts with | "values.name_startsWith": "Web" |
_gt / _gte |
Greater than / >= | "values.price_gte": 100 |
_lt / _lte |
Less than / <= | "values.price_lt": 500 |
_in |
In array | "values.status_in": ["active", "featured"] |
Sort is a Record<string, "asc" | "desc"> object:
sort: { "values.name": "asc" } // alphabetical
sort: { "values.price": "desc" } // highest price first
sort: { "values.createdOn": "desc" } // newest first
Use where with either id (revision ID) or entryId:
// By revision ID
const result = await webiny.cms.getEntry<Product>({
modelId: "product",
where: { id: "abc123#0001" },
fields: ["id", "entryId", "values.name", "values.price"]
});
// By entry ID (gets latest published revision)
const result = await webiny.cms.getEntry<Product>({
modelId: "product",
where: { entryId: "abc123" },
fields: ["id", "entryId", "values.name"]
});
Pass preview: true to listEntries or getEntry to access unpublished/draft content:
const result = await webiny.cms.listEntries<Product>({
modelId: "product",
fields: ["id", "entryId", "values.name"],
preview: true // returns drafts + published
});
CRITICAL: Content fields MUST be wrapped inside a
valueskey in thedataobject. Passing fields directly (withoutvalues) will result in an empty or malformed entry.
const result = await webiny.cms.createEntry({
modelId: "contactSubmission",
data: {
values: {
// ← REQUIRED: wrap all content fields in `values`
name: "John Doe",
email: "john@example.com",
message: "Hello from the contact form!"
}
},
fields: ["id", "entryId"]
});
The method is updateEntryRevision, not updateEntry. Use revisionId (the full entryId#revisionNumber, e.g. "abc123#0001"):
const result = await webiny.cms.updateEntryRevision({
modelId: "product",
revisionId: "abc123#0001", // ← note: revisionId, not id
data: {
values: {
// ← REQUIRED: wrap fields in `values`
price: 29.99
}
},
fields: ["id", "entryId", "values.price"]
});
The methods are publishEntryRevision and unpublishEntryRevision, not publishEntry/unpublishEntry. Both require revisionId and fields:
await webiny.cms.publishEntryRevision({
modelId: "product",
revisionId: "abc123#0001",
fields: ["id", "entryId", "status"]
});
await webiny.cms.unpublishEntryRevision({
modelId: "product",
revisionId: "abc123#0001",
fields: ["id", "entryId", "status"]
});
await webiny.cms.deleteEntryRevision({
modelId: "product",
revisionId: "abc123#0001",
fields: []
});
webiny.languages.listLanguages() returns all enabled languages — disabled languages are always filtered out server-side, so no filter parameter is needed.
import type { Language } from "@webiny/sdk";
const result = await webiny.languages.listLanguages();
if (result.isOk()) {
const languages: Language[] = result.value;
// languages[0].code, .name, .direction, .isDefault
}
The Language type:
interface Language {
id: string;
code: string; // e.g. "en-US"
name: string; // e.g. "English (US)"
direction?: "ltr" | "rtl";
isDefault?: boolean;
}
// List files
const files = await webiny.fileManager.listFiles({
limit: 20,
fields: ["id", "key", "name", "size", "type", "src"]
});
// Upload a file (returns presigned URL for direct S3 upload)
const uploaded = await webiny.fileManager.uploadFile({ file: myFile });
For programmatic access, create API keys as an extension:
// extensions/MyApiKey.ts
import { ApiKeyFactory } from "webiny/api/security";
class MyApiKeyImpl implements ApiKeyFactory.Interface {
execute(): ApiKeyFactory.Return {
return [
{
name: "Universal API Key",
slug: "universal-key",
token: "wat_12345678",
permissions: [{ name: "*" }]
}
];
}
}
export default ApiKeyFactory.createImplementation({
implementation: MyApiKeyImpl,
dependencies: []
});
Register (YOU MUST include the .ts file extension in the src prop — omitting it will cause a build failure):
<Api.Extension src={"/extensions/MyApiKey.ts"} />
webiny.tasks wraps the Background Tasks GraphQL API. All methods return a Result and never throw.
Returns all registered task definitions — use this to discover valid definition IDs before triggering.
const result = await webiny.tasks.listDefinitions();
if (result.isOk()) {
// result.value: TaskDefinition[]
for (const def of result.value) {
console.log(def.id, def.title, def.description);
}
}
const result = await webiny.tasks.listTasks();
if (result.isOk()) {
// result.value: TaskRun[]
for (const task of result.value) {
console.log(task.id, task.taskStatus, task.definitionId);
}
}
Optionally filter by a specific task run ID:
// All logs
const result = await webiny.tasks.listLogs();
// Logs for a specific task run
const result = await webiny.tasks.listLogs({
where: { task: "yourTaskRunId" }
});
if (result.isOk()) {
for (const log of result.value) {
for (const item of log.items) {
console.log(`[${item.type}] ${item.message}`);
}
}
}
const result = await webiny.tasks.triggerTask({
definition: "myTaskDefinitionId",
input: {
someVariable: "someValue",
anotherVariable: 42
}
});
if (result.isOk()) {
const task = result.value; // TaskRun
console.log(task.id, task.taskStatus, task.executionName);
}
The task stops at its next safe checkpoint.
const result = await webiny.tasks.abortTask({
id: "yourTaskRunId",
message: "Stopped by user request" // optional
});
if (result.isOk()) {
console.log(result.value.taskStatus); // "aborted"
}
import type { TaskDefinition, TaskRun, TaskLog, TaskLogItem, TaskStatus } from "@webiny/sdk";
type TaskStatus = "pending" | "running" | "completed" | "failed" | "aborted" | "stopped";
interface TaskDefinition {
id: string;
title: string;
description?: string;
}
interface TaskRun {
id: string;
definitionId: string;
taskStatus: TaskStatus;
input?: unknown;
output?: unknown;
startedOn?: string;
finishedOn?: string;
executionName?: string;
iterations?: number;
parentId?: string;
}
| Module | Webiny App | What You Can Do |
|---|---|---|
webiny.cms |
Headless CMS | List, get, create, update, publish, unpublish, delete entry revisions |
webiny.fileManager |
File Manager | List, upload, and manage files and folders |
webiny.tenantManager |
Multi-tenancy | Create, install, enable, disable tenants |
webiny.languages |
Languages | List enabled languages (id, code, name, direction, isDefault) |
webiny.tasks |
Background Tasks | Trigger, abort, list task runs, definitions, and logs |
| Mistake | Correct |
|---|---|
data: { name: "..." } |
data: { values: { name: "..." } } |
updateEntry(...) |
updateEntryRevision(...) |
publishEntry(...) |
publishEntryRevision(...) |
unpublishEntry(...) |
unpublishEntryRevision(...) |
sort: ["values.name_ASC"] |
sort: { "values.name": "asc" } |
getEntry({ id: "..." }) |
getEntry({ where: { id: "..." } }) |
Omitting fields |
Always provide fields: [...] |
| Trailing slash in endpoint | Remove trailing slash from endpoint URL |
triggerTask with unknown definition string |
Use an ID returned by listDefinitions() — the GQL schema validates it against WebinyBackgroundTaskDefinitionEnum! |
Install: npm install @webiny/sdk
Import: import { Webiny } from "@webiny/sdk";
Type import: import type { CmsEntryData, TaskRun } from "@webiny/sdk";
Initialize: new Webiny({ token, endpoint, tenant })
Result check: result.isOk() -> result.value / result.error.message
API endpoint: yarn webiny info (in your Webiny project) -- NO trailing slash
Preview mode: pass preview: true to listEntries / getEntry
fields required: every CMS method needs a fields: string[] array
values wrapper: createEntry/updateEntryRevision data must use { values: { ... } }
Background tasks: webiny.tasks.triggerTask({ definition, input })
Abort task: webiny.tasks.abortTask({ id, message? })
Filter logs by task: webiny.tasks.listLogs({ where: { task: "id" } })
webiny-api-cms-content-models -- Define the models you query with the SDKwebiny-website-builder -- Use the SDK inside Website Builder components to fetch CMS data