Agent Actions patterns
Explore common patterns across all Agent Actions
Agent Actions offer an interface to enhance Sanity documents with the use of large language models (LLMs). This document showcases a collection of common patterns and concepts that apply to all Agent Actions.
Prerequisites:
- Complete the quick start for one or more of the Agent Actions.
@sanity/clientv7.1.0 or later and an environment to run client requests.- API version vX or later for any requests using Agent Actions.
Generate quick start
Get started with Generate by writing your first instructions to create and modify documents.
Transform quick start
Get started with Transform by writing your first instructions to modify documents.
Translate quick start
Learn to translate documents with the Translate Agent Actions action.
Many examples in this document use @sanity/client and expect that you've installed and configured it for your project. If your client is named something other than client, update the code examples accordingly.
Here's an example of the client implementation:
// client.ts
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: '<project-id>',
dataset: '<dataset-name>',
useCdn: 'true',
apiVersion: 'vX',
token: '<read-write-token>'
})Then, import client in your code before using the examples below.
Multiple action examples
Some examples in this guide are for specific Agent Actions, so the code may differ slightly. For instance, Translate doesn't have an instruction concept in the same way Generate does, but the techniques in each example are the same regardless of action.
Use noWrite to avoid mutations
The noWrite property prevents the instruction from writing changes to your dataset. This is useful when creating in-memory documents, previewing changes, or working with multiple requests before making a final write. You could even use it to combine multiple Agent Actions.
All Agent Actions support the noWrite option.
const response = await client.agent.action.generate({
schemaId: "your-schema-id",
noWrite: true,
targetDocument: {operation: 'create', _type: 'movie'},
instruction: "Write the details for a movie titled $title.",
instructionParams: {
title: { type: "constant", value: "Sanity: The Content Operating System" },
},
});
console.log(response);const response = await client.agent.action.transform({
schemaId: "your-schema-id",
noWrite: true,
documentId: '<source-document-id>',
instruction: "Replace every instance of 'Create' with 'Canvas'",
});
console.log(response);const response = await client.agent.action.translate({
schemaId: 'your-schema-id',
documentId: '<source-document-id>',
noWrite: true,
fromLanguage: { id: 'en-GB',title: 'English' },
toLanguage: { id: 'no-NB', title: 'Norwegian Bokmål' },
})Instead of creating or modifying a document in your dataset, this code returns the document to the response constant and logs it to the console.
Keep in mind that these requests still count against your usage limits.
Enable read/write on conditional fields
By default, Agent Actions ignore conditional readOnly and hidden fields. You can allow actions to interact with these fields by setting the conditionalPaths parameter. The examples below use the generate syntax, but the conditionalPaths configuration is the same across all actions.
To enable access to all hidden and readOnly fields across your schema, use the following to change the default behavior:
await client.agent.action.generate({
schemaId: 'your-schema-id',
targetDocument: {operation: 'create', _type: '<document-type>'},
instruction: `<insert instruction here>`,
instructionParams: { ... },
conditionalPaths: {
defaultReadOnly: false,
defaultHidden: false
}
})You can also limit read/write to specific paths. Add additional paths to the array as needed.
await client.agent.action.generate({
schemaId: 'your-schema-id',
targetDocument: {operation: 'create', _type: '<document-type>'},
instruction: `<insert instruction here>`,
instructionParams: { ... },
conditionalPaths: {
paths: [
{
path: ['secretPathName'],
readOnly: false,
hidden: false,
}
]
}
})Exclude a field from an action
Agent Actions skip any field the schema marks as hidden or readOnly. That gives you a schema-level way to keep a field away from the AI, without changing the requests that run against the document.
import { defineField } from 'sanity'
defineField({
name: 'legalDisclaimer',
title: 'Legal disclaimer',
type: 'string',
// Editors can still read this field, but no action will write to it.
readOnly: true,
})Use readOnly: true when editors should still see the field, and hidden: true when it shouldn't appear in the Studio at all. Either way, the action leaves the field alone.
To decide per request instead of per schema, make the condition a function. Conditional hidden and readOnly fields are excluded by default, so hidden: () => true keeps a field out of every action until a request opts back in with conditionalPaths.
defineField({
name: 'internalNotes',
title: 'Internal notes',
type: 'text',
hidden: () => true,
})await client.agent.action.generate({
schemaId: 'your-schema-id',
documentId: '<document-id>',
instruction: 'Summarize the article into the internal notes field.',
conditionalPaths: {
paths: [{ path: ['internalNotes'], readOnly: false, hidden: false }],
},
})aiAssist.exclude doesn't apply here
options.aiAssist.exclude turns a field off for the AI Assist plugin in the Studio. It has no effect on Agent Actions run through client.agent.action. Use hidden or readOnly instead.
Copy a field without translating it
Translate with a targetDocument runs in two steps: it copies the source document to the target, then translates the target. The copy step isn't schema-aware, so it transfers every field, including the ones the translation step skips. A field marked hidden or readOnly arrives in the translated document with its source value intact.
Excluding the field with target.exclude behaves differently. That filter applies to both steps, so the field is never copied and never reaches the translated document.
await client.agent.action.translate({
schemaId: 'your-schema-id',
documentId: '<source-document-id>',
targetDocument: { operation: 'create' },
fromLanguage: { id: 'en-US', title: 'English' },
toLanguage: { id: 'el-GR', title: 'Greek' },
// Left out of both the copy and the translation.
target: { exclude: ['internalNotes'] },
})If you use the document internationalization plugin, Translate also skips fields marked with options.documentInternationalization.exclude. Those fields are still copied to the target document. Prefer hidden or readOnly when the plugin isn't part of your setup.
Asynchronously modify multiple documents
The async parameter helps initiate instructions and move on. Asynchronous calls to actions return a document ID rather than the complete document shape. In this example:
- We loop through the IDs of documents from a GROQ request.
asyncis set to true to enable asynchronous requests.- The instruction rewrites the title of each document.
const ids = await client.fetch(`*[_type == 'movie' ][0...5] { _id }`);
for (const id of ids) {
await client.agent.action.generate({
schemaId: "your-schema-id",
documentId: id._id,
instruction: `Re-imagine the title, $title, so that it is more engaging and interesting.`,
async: true,
path: "title",
instructionParams: {
title: {
type: "field",
path: "title",
},
},
});
}Protip
We're calling this an asynchronous call, but we're also using await. That's because the asynchronous call happens behind the scenes. We aren't waiting on the AI to finish and Content Lake to update. Instead, we're waiting on the underlying request to Sanity to respond that it initiated those actions.
Note that you can't combine async and noWrite, as noWrite would require the request to wait for a response from the AI.
Target specific fields
Restrict the fields that actions can write to by setting a single path or multiple include fields in the target parameter.
To write to a single path, or all child paths of a single parent, use the target property with path.
await client.agent.action.generate({
schemaId: "your-schema-id",
targetDocument: {operation: 'create', _type: 'movie'},
instruction: `Your instruction here.`,
// ... other properties
target: {
path: "body", // set to whichever field or fieldset you like. Ex. 'title', 'name', etc.
}
});To define specific fields, use include. They will be relative to a path, if set, or the document if not set. In the example below, they are relative to the document.
await client.agent.action.generate({
schemaId: "your-schema-id",
targetDocument: {operation: 'create', _type: 'movie'},
instruction: `Your instruction here.`,
// ... other properties
target: {
include: ["title", "overview", "poster"],
},
});You can also do the same to exclude any paths you want to block the instruction from mutating.
await client.agent.action.generate({
schemaId: 'your-schema-id',
targetDocument: {operation: 'create', _type: 'movie'},
instruction: `Your instruction here.`,
// ... other properties
target: {
exclude: ['humanOnlyField']
}
})Target patterns
The following are an assortment of examples using target and its options. For Transform and Translate, omit the Generate instruction and add the source documentId property.
/*
using path
this sets 'title' field
*/
{
targetDocument: {operation: 'create', _type: 'article'},
schemaId: 'your-schema-id',
instruction: 'A title for an article about dogs',
target: {path: ['title']}
}
/*
using include
this sets:
- title
- description
*/
{
targetDocument: {operation: 'create', _type: 'article'},
schemaId: 'your-schema-id',
instruction: 'Stuff about dogs',
target: {include: ['title', 'description']},
}
/*
this sets:
- objectField.title
- objectField.description
*/
{
targetDocument: {operation: 'create', _type: 'article'},
schemaId: 'your-schema-id',
instruction: 'Stuff about dogs',
target: {path: ['objectField'], include: ['title', 'description']}
}
/*
multiple target paths
this sets:
- objectField.title
- objectField.description
- people[_key=="someKey"].name //ie, the name of a single item in the people array
*/
{
targetDocument: {operation: 'create', _type: 'article'},
schemaId: 'your-schema-id',
instruction: 'Stuff about dogs',
target: [
{path: ['objectField'], include: ['title', 'description']},
{path: ['people', {_key: 'someKey'}], include: ['name']}
]
}
/*
Deeply nested fields from a common target path.
This sets:
- objectField.nestedObject.title
- objectField.otherObject.deeplyNested
- all its children(assuming deeplyNested is an object)
*/
{
targetDocument: {operation: 'create', _type: 'article'},
schemaId: 'your-schema-id',
instruction: 'Stuff about dogs',
target: {
path: 'objectField',
include: [
{path: ['nestedObject', 'title']},
{path: ['otherObject', 'deeplyNested']}
]
}
}Agent Actions in field actions
Agent Actions can leverage custom AI Assist field actions to create on-demand features for content editors directly in Studio. Learn more in the custom field actions documentation, or jump right into the field action patterns.