Create, Update, and Delete Content
Content mutations enable full programmatic content lifecycle management through the GCA, allowing you to create, update, archive, publish, and delete content items via GraphQL operations. By configuring content types as mutable (via mutableEntryClass) and selecting which write actions to expose (via contentActionType), you get mutations that respect Brightspot's workflow, permissions, and validation rules.
Mutations handle complex scenarios including uploading and managing images and files (StorageItem fields), setting references to other content items, saving embedded objects, and executing multiple writes within a transaction. The mutation input shapes mirror your Java content type structure, and responses return the same content model types used by Get and Query.
Configuration
Two settings work together: mutableEntryClass marks which types can be written, and contentActionType selects which write operations appear in the schema:
18import com.psddev.graphql.gca.schema.types.db.write.delete.DeleteActionDefinition;9import com.psddev.graphql.gca.schema.types.db.write.save.SaveActionDefinition;1011@Recordable.DisplayName("Content API")12public class ContentApiEndpoint extends GCAEndpoint implements Singleton {131419@Override20protected GCASchemaSettings getSchemaSettings() {21return GCASchemaSettings.newBuilder()22.readonlyEntryClass(Author.class)23.mutableEntryClass(Article.class)24.contentActionType(SaveActionDefinition.class)25.contentActionType(ArchiveActionDefinition.class)26.contentActionType(DeleteActionDefinition.class)27.build();28}29}
Use contentActionTypesAll() to automatically include every available action, including any custom WriteOperationDefinition implementations in your project.
Anatomy of a mutation
All content writes live under the root mutation's Content field:
Content/Action/<Action>/Record(context, args)— perform a single write action.Content/Transaction(context, writes)— perform multiple writes atomically. See Transactions.
Every action takes two arguments:
context(ContentActionContextInput) — cross-cutting execution options, most notablydryRun(see Dry runs).args— the action-specific input. For all actions except Save, this is justwith, the record lookup. The lookup input works exactly like Get's: by_id, by URL, or by a unique field.
Available actions
| Action | Arguments | Result fields | Description |
|---|---|---|---|
Save | with (optional), main, embeds, urls | state, saveInfo | Creates new or updates existing content. |
Publish | with | state | Moves content to the Published state. |
Archive | with | archived, state | Soft delete—flags the record as archived, hiding it from default queries. |
Restore | with, restoreTo: DRAFT | LIVE | restored, state | Restores archived content back to draft or live. |
Delete | with | deleted, state | Permanently deletes the record from the database. |
Workflow | with, transition, comment | log, state | Moves content through a workflow transition, recording a WorkflowLog. |
Every result exposes state—the record after the action—so you can confirm exactly what was written.
Saving content
The Save action both creates and updates. Its main argument is a diff: an input object named <Type>Diff containing only the fields you want to change. Omit with to create new content; provide it to update an existing record:
- GraphQL Mutation
- GraphQL Variables
- GraphQL Response
1mutation SaveArticle($context: ContentActionContextInput = {}, $args: RecordSaveActionInput!) {2Content {3Action {4Save {5Record(context: $context, args: $args) {6state {7__typename8_id9... on Article {10headline11tags12author {13_id14}15}16}17}18}19}20}21}22
1{2"context": {3// !tooltip[/dryRun/] Validate and preview the result of the save without persisting anything.4"dryRun": false5},6"args": {7// !tooltip[/main/] The diff to apply. Omitting `with` creates new content; include `with` to update an existing record. Only the fields present in the diff are changed.8"main": {9"ArticleDiff": {10"headline": "Welcome to Brightspot",11"body": "Hello world!",12"tags": ["news", "welcome"],13// !tooltip[/author/] References to other records use the same lookup input as Get: by `_id`, `_path`, or any unique field.14"author": {15"_id": "0000019b-aa01-d394-adbf-aa01280f0000"16}17}18}19}20}21
1{2"data": {3"Content": {4"Action": {5"Save": {6"Record": {7"state": {8"__typename": "Article",9"_id": "0000019e-b886-db61-a5df-faa7b5ef0000",10"headline": "Welcome to Brightspot",11"tags": ["news", "welcome"],12"author": {13"_id": "0000019e-b886-db61-a5df-faa7b5b20000"14}15}16}17}18}19}20}21}22
Fields not present in the diff are untouched, which makes Save safe for concurrent, partial updates—two clients updating different fields won't clobber each other.
Setting references
Reference fields (like Article.author) accept the same one-of lookup input used everywhere else: {_id: ...}, a URL, or a unique field value. The referenced record must already exist—or be created in the same request and linked by named reference.
Linking embedded and new records
Embedded records (@Embedded types stored inside their parent) are created through the Save action's embeds argument—a list of diffs alongside main. The parent field links to entries in that list in one of two ways:
{_index: N}— by position in theembedsarray.{_ref: "name"}— by a_nameyou assign inside the embedded diff.
1{2"context": {},3"args": {4"main": {5"AuthorDiff": {6"name": "Jane Doe",7"email": "jane@example.com",8"addresses": [9// !tooltip[/_index/] Link to the embedded object by its position in the `embeds` array below.10{"_index": 0},11// !tooltip[/_ref/] Link to the embedded object by the `_name` you assigned it in the `embeds` array.12{"_ref": "office"}13]14}15},16"embeds": [17{18"AddressDiff": {"street": "21 Jump Street", "city": "Reston"}19},20{21// !tooltip[/_name/] An arbitrary name that other diffs in this mutation can reference via `_ref`.22"AddressDiff": {"_name": "office", "street": "22 Jump Street", "city": "Reston"}23}24]25}26}27
To update an existing embedded record, reference it by its {_id: ...} instead.
Uploading files and images
StorageItem fields accept a structured input with a one-of data field plus optional metadata:
| Input | Description |
|---|---|
data.file | A multipart file upload—binary data sent with the request. Stored in your configured storage backend with metadata (dimensions, EXIF/IPTC) extracted automatically. |
data.url | A URL the system downloads, stores, and extracts metadata from. |
data.urlRef | A URL stored as-is—no download, no metadata extraction. |
metadata | Merge (put) or replace custom metadata entries on the item. |
1{2"context": {},3"args": {4"with": {5"_id": "0000019b-a37e-ddc0-af9f-b37edded0000"6},7"main": {8"ArticleDiff": {9"leadImage": {10"data": {11// !tooltip[/url/] The file at this URL is downloaded, stored in your configured storage backend, and its metadata (dimensions, EXIF, etc.) is extracted. Use `urlRef` instead to reference the URL as-is without downloading, or `file` with a multipart upload to send binary data directly.12"url": "https://example.com/images/lead.jpg"13},14// !tooltip[/metadata/] Optionally merge custom metadata into the stored item.15"metadata": {16"put": [{17"key": "credit",18"value": "Jane Doe"19}]20}21}22}23}24}25}26
Detecting conflicting writes
Save accepts a readTimestamp argument: pass the lastUpdate value from the Get query you used to fetch the record. If any field you're modifying changed after that time, the save fails instead of silently overwriting someone else's work—your application can then re-fetch and retry.
Archive vs. Delete
These are commonly confused:
- Archive is a save that flags the record (the CMS "Archive" / trash behavior). The record remains in the database, disappears from default queries, and can be brought back with Restore.
- Delete permanently removes the record. There is no undo.
- GraphQL Mutation
- GraphQL Variables
- GraphQL Response
1mutation ArchiveArticle($context: ContentActionContextInput = {}, $args: RecordArchiveActionInput!) {2Content {3Action {4Archive {5Record(context: $context, args: $args) {6archived7}8}9}10}11}12
1{2"context": {3// !tooltip[/dryRun/] Validate and preview the result of the archive without persisting anything.4"dryRun": false5},6"args": {7"with": {8"_id": "0000019b-a37e-ddc0-af9f-b37edded0000"9}10}11}12
1{2"data": {3"Content": {4"Action": {5"Archive": {6"Record": {7"archived": true8}9}10}11}12}13}14
Dry runs
Pass context: {dryRun: true} to execute the full mutation—validation, hooks, computed state—without persisting anything. The response includes the would-be result plus a dryRuns entry in the response extensions identifying which operations were rolled back:
1"extensions": {2"dryRuns": [{ "path": "/Content/Action/Save/Record" }]3}
Dry runs are invaluable for validating editorial input in custom tools before committing, and for testing migrations safely.
Transactions
Content/Transaction executes a list of write operations inside a single database transaction—either all succeed and commit, or the transaction is rolled back. Each entry in the writes list is a one-of input selecting an action with the same args shape as the standalone version, and the result exposes a corresponding list of per-write results.
Within a transaction:
- All writes observe the same timestamp (
database.now()is pinned for the duration). - Named references (
_ref) are shared across the whole request, so one write can reference content created by another. dryRunapplies to the entire transaction.
Attribution, sites, and permissions
By default, mutations are unattributed. Two supplier settings integrate writes with Brightspot's editorial model:
toolUserSupplier(...)— supplies theToolUserto attribute saves to (publish user, update user). With attribution in place, published saves also generateHistoryrevisions, visible via Get's Revision field.siteSupplier(...)— supplies the currentSite. New content is owned by that site, and writes to content not accessible from that site are rejected.
Mutations also run Brightspot's standard validation: @Required fields, custom beforeSave logic, and write hooks all apply. Validation failures surface as GraphQL errors; enable verboseClientErrors() during development for more detail (it may expose sensitive data, so leave it off in production).
Next steps
- Content Schema Types — how input types are generated from your Java classes
- Get Single Content Items — fetch what you've written, including revisions
- Security — locking down who can call mutations