Writing data
Create, update, duplicate, and delete documents and globals from Go, TypeScript, and REST, and keep concurrent edits from overwriting each other.
Writes go through the same engine as reads, whichever API you use: access rules, validation and hooks run every time, and the response is the saved document. Examples use the example schema and the same tab setup as Querying data.
Create
A new post:
post, err := local.Create(ctx, "posts", store.Values{
"title": store.String("Draft idea"),
"tags": store.List(store.String("idea")),
}, ridu.MutationOptions{Actor: user})const post = await ridu.create('posts', {
title: 'Draft idea',
tags: ['idea']
});curl -X POST "$RIDU/api/collections/posts" \
-H "Authorization: Session $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Draft idea","tags":["idea"]}'{
"id": "posts_4c1a7d85841d75a1d744f90f",
"title": "Draft idea",
"status": "draft",
"tags": ["idea"],
"createdAt": "2026-10-06T13:48:20.726903Z",
"updatedAt": "2026-10-06T13:48:20.726903Z"
}Fields you leave out get their defaults, so status is draft. Over REST the document arrives as
{ "doc": { … } }; Go and TypeScript return the document itself. In Go, build values with
store.String, store.Number, store.List and the other
value constructors, or use typed handles.
Update
Send only the fields you want to change. The rest keep their stored values.
Retitle a post:
post, err := local.Update(ctx, "posts", postID, store.Values{
"title": store.String("A better idea"),
}, ridu.MutationOptions{Actor: user})const post = await ridu.update('posts', postID, {
title: 'A better idea'
});curl -X PATCH "$RIDU/api/collections/posts/$POST_ID" \
-H "Authorization: Session $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"A better idea"}'To clear a field, send null (store.Null() in Go).
Update a versioned document
A collection with Versions: true keeps every saved revision, and a published document only changes
by being published again. Send your changes to the publish operation, with the revision you last
read:
// page is the document you read before editing.
updated, err := local.PublishChanges(ctx, "pages", page.ID,
store.Values{"title": store.String("About us")},
ridu.MutationOptions{
ExpectedRevision: page.Revision,
Actor: user,
},
)const updated = await ridu.publishChanges(
'pages',
page.id,
{ title: 'About us' },
{ revision: page._revision }
);curl -X POST "$RIDU/api/collections/pages/$PAGE_ID/publish" \
-H "Authorization: Session $TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "1"' \
-d '{"title":"About us"}'A plain update of a published document is refused with publish_required (409), and the message
names the publish route. With drafts enabled, you can also save changes as a draft first; see
Drafts and versions.
Don’t overwrite someone else’s edit
The revision makes the write conditional. If someone saved the document after you read it, its
revision has moved on and your write fails with conflict (409) instead of replacing their
changes:
{
"error": {
"code": "conflict",
"status": 409,
"message": "document conflicts with an existing value"
}
}Reload the document, show the newer version, and let the author decide. Pass the revision on every interactive edit; document locks coordinate editors but don’t replace it. Leaving it out, or passing zero in Go, writes without the check.
Delete
_, err := local.Delete(ctx, "posts", postID, ridu.MutationOptions{
Actor: user,
})await ridu.delete('posts', postID);curl -X DELETE "$RIDU/api/collections/posts/$POST_ID" \
-H "Authorization: Session $TOKEN"{ "id": "posts_0354693e1c584b9c537d890e", "deleted": true }In a collection with trash, delete moves the document to the trash instead; restore and permanent delete are separate operations. See Bulk actions and trash.
Duplicate
Copy a document, optionally changing some fields:
copied, err := local.Duplicate(ctx, "posts", postID, store.Values{
"title": store.String("Copy of a better idea"),
}, ridu.MutationOptions{Actor: user})const copied = await ridu.duplicate('posts', postID, {
title: 'Copy of a better idea'
});curl -X POST "$RIDU/api/collections/posts/$POST_ID/duplicate" \
-H "Authorization: Session $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Copy of a better idea"}'The copy is a new document with its own ID. Access rules and hooks run as for a create.
Update a global
A global has no create or delete: updating it the first time saves it.
settings, err := local.UpdateGlobal(ctx, "site-settings",
store.Values{"siteName": store.String("Acme")},
ridu.MutationOptions{Actor: user},
)const settings = await ridu.updateGlobal('site-settings', {
siteName: 'Acme'
});curl -X PATCH "$RIDU/api/globals/site-settings" \
-H "Authorization: Session $TOKEN" \
-H "Content-Type: application/json" \
-d '{"siteName":"Acme"}'Before its first save, a global reads as its defaults. A versioned global changes through
PublishGlobalChanges (publishGlobalChanges in TypeScript), like a
versioned document.
Change several documents at once
Set two posts back to draft:
ids := []string{firstID, secondID}
posts, err := local.BulkUpdate(ctx, "posts", ids,
store.Values{"status": store.String("draft")},
ridu.BulkOptions{Actor: user},
)const posts = await ridu.bulkUpdate('posts', [firstID, secondID], {
status: 'draft'
});curl -X POST "$RIDU/api/collections/posts/bulk" \
-H "Authorization: Session $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<JSON
{"action":"update","ids":["$FIRST_ID","$SECOND_ID"],
"data":{"status":"draft"}}
JSONA bulk action takes 1 to 100 IDs and saves all of them or none. Each document still passes its own access rules, validation and hooks. The same form covers publishing, unpublishing, deleting, restoring and permanently deleting several documents; see Bulk actions and trash.
When a write fails
A write that breaks a rule saves nothing and fails with a code you can branch on:
| Code | Status | Means |
|---|---|---|
validation |
422 |
A field is invalid; issues lists each one with its path |
conflict |
409 |
A stale revision, or a unique value already in use |
publish_required |
409 |
A plain update of a published versioned document; publish the changes instead |
access_denied |
403 |
The caller may not make this change |
not_found |
404 |
The document doesn’t exist, or the caller can’t see it |
rejected |
422 |
A hook refused the change; its message is written for people |
Handle them in Go with *ridu.OperationError, in TypeScript with
RiduError, and over REST from the
error envelope.