Using Saved Objects services
This page describes how to perform CRUD operations (create, get, search, update, delete) on Saved Object instances of a given type from server-side plugin code. Use the Saved Objects service exposed by Core; do not use the HTTP APIs under /api/saved_objects/, which are deprecated and will be removed.
The HTTP routes registered under /api/saved_objects/ (and /internal/saved_objects/ for migration and cleanup) are deprecated. Plugins must not rely on them for create, get, find, update, delete, bulk operations, export, or import. Use the Core Saved Objects service from your plugin's request handlers or other server-side code instead.
Core exposes the Saved Objects service to plugins as core.savedObjects:
- In your plugin's
setup(core, plugins)hook,core.savedObjectsis SavedObjectsServiceSetup. Use it to register types (registerType), set client factory, and configure extensions. It does not provide clients or repositories. - In your plugin's
start(core, plugins)hook,core.savedObjectsis SavedObjectsServiceStart. This interface is defined insrc/core/packages/saved-objects/server/src/contracts.ts. Use it to obtain a scoped or internal client, or a scoped or internal repository, and to perform CRUD operations on Saved Objects.
You need the start contract to create, read, update, or delete Saved Object documents. Typically you obtain a client (or repository) per request inside a route handler using the request from the handler.
SavedObjectsServiceStart (contracts.ts) exposes four methods. The first two return an object that implements SavedObjectsClientContract (defined in src/core/packages/saved-objects/api-server/src/saved_objects_client.ts). The other two return an object that implements ISavedObjectsRepository (defined in src/core/packages/saved-objects/api-server/src/saved_objects_repository.ts).
| Method | Returns | Scoped to request? | Use when |
|---|---|---|---|
getScopedClient(req, options?) |
Object implementing SavedObjectsClientContract |
Yes (user from req) |
Normal user-driven operations; respects security and spaces. |
getUnsafeInternalClient(options?) |
Object implementing SavedObjectsClientContract |
No (internal user) | System-level operations; no user context; security extension excluded. |
createScopedRepository(req, includedHiddenTypes?, extensions?) |
Object implementing ISavedObjectsRepository |
Yes | You need repository-only methods (e.g. incrementCounter, deleteByNamespace) with request context. |
createInternalRepository(includedHiddenTypes?, extensions?) |
Object implementing ISavedObjectsRepository |
No | Internal operations needing repository-only methods, no user context. |
Creates a Saved Objects client scoped to the given KibanaRequest. The client uses the requesting user's credentials and applies security and spaces extensions. Use this for all user-facing operations (e.g. in HTTP route handlers).
- Options:
SavedObjectsClientProviderOptions—includedHiddenTypes?: string[]to allow access to hidden types,excludedExtensions?: string[]to disable specific extensions for this client. - When to use: Default choice for request handlers. Prefer this over creating a repository unless you need methods only available on the repository.
Creates a Saved Objects client that uses the internal Kibana user. It does not run with a specific user context; the security extension is always excluded so there is no user-based filtering. Other extensions (encryption, spaces) can still be applied. Use only for system-level operations (e.g. background jobs, migrations, or when no request is available).
- Options:
SavedObjectsClientProviderOptions—includedHiddenTypes?: string[],excludedExtensions?: string[]. The security extension is excluded regardless. - When to use: When you must perform Saved Object operations without a request or with system privileges. Do not use with a fake request to bypass security.
Creates a repository scoped to the given request. The repository has all client methods plus a few extra operations (see Working with the SavedObjectsRepository). Use only when you need those repository-only methods with request context.
- Parameters:
req, optionalincludedHiddenTypes, optionalextensions. - When to use: When you need
incrementCounter,deleteByNamespace, orfindwith internal options. PrefergetScopedClientotherwise.
Creates a repository using the internal user, with no request context. Same as getUnsafeInternalClient in terms of scoping (system-level, no user), but returns a repository so you can use repository-only methods.
- Parameters: Optional
includedHiddenTypes, optionalextensions. - When to use: When you need repository-only methods for internal operations (e.g. usage collection with
incrementCounter). PrefergetUnsafeInternalClientif the client API is enough.
- Use the client (
getScopedClientorgetUnsafeInternalClient) for almost all use cases: CRUD, find, search, resolve, bulk operations, point-in-time finder, updateObjectsSpaces, removeReferencesTo, and access control (changeOwnership, changeAccessMode). The client is the recommended API and covers the vast majority of plugin needs. - Use the repository (
createScopedRepositoryorcreateInternalRepository) only when you need one of the following:incrementCounter— Atomically increment numeric fields (e.g. for usage collection). The client does not expose this.deleteByNamespace— Delete all Saved Objects in a given space (e.g. when deleting a space). The client does not expose this.findwith internal options — Advanced cases where you need to run find with extensions disabled (internal use; rarely needed by plugins).
If you are not sure, use the client. Prefer the scoped client for request handlers and the internal client for background or system operations.
getScopedClient and getUnsafeInternalClient return an object that implements SavedObjectsClientContract (see src/core/packages/saved-objects/api-server/src/saved_objects_client.ts). That interface is the main API for reading and writing Saved Objects from a plugin. Use it in route handlers and services whenever you have a request (scoped client) or need system-level access (internal client).
- Create:
create(type, attributes, options?),bulkCreate(objects, options?),checkConflicts(objects, options?) - Read:
get(type, id, options?),bulkGet(objects, options?),resolve(type, id, options?),bulkResolve(objects, options?) - Search / list:
find(options),search(options)(raw Elasticsearch-style search; use with care),createPointInTimeFinder(findOptions, deps?)for large result sets - Update:
update(type, id, attributes, options?),bulkUpdate(objects, options?) - Delete:
delete(type, id, options?),bulkDelete(objects, options?) - Spaces and references:
updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options?),removeReferencesTo(type, id, options?),collectMultiNamespaceReferences(objects, options?) - Point-in-time:
openPointInTimeForType(type, options?),closePointInTime(id, options?) - Access control:
changeOwnership(objects, options),changeAccessMode(objects, options)(when the type supports access control) - Convenience:
getCurrentNamespace(),asScopedToNamespace(namespace)(returns a new client scoped to that namespace)
Options typically include namespace for space-scoped types. Create options can include references, overwrite, and id. See the type definitions for full option shapes.
Route handler: scoped client (recommended)
router.get(
{ path: '/api/my-plugin/dashboards', validate: false },
async (context, req, res) => {
const client = context.core.savedObjects.getClient();
const { saved_objects } = await client.find({
type: 'dashboard',
perPage: 20,
});
return res.ok({ body: saved_objects });
}
);
Get, update, and delete
const client = savedObjectsStart.getScopedClient(req);
const dashboard = await client.get('dashboard', id);
await client.update('dashboard', id, { title: 'New title' });
await client.delete('dashboard', id);
Create with references
const client = context.core.savedObjects.getClient();
const created = await client.create(
'dashboard',
{ title: 'My Dashboard', panels: [{ visualizationRefName: 'vis1' }] },
{
references: [
{ id: 'vis-id-123', type: 'visualization', name: 'vis1' },
],
}
);
Accessing hidden types
If your type is registered with hidden: true, pass it in includedHiddenTypes:
const client = savedObjectsStart.getScopedClient(req, {
includedHiddenTypes: ['my-plugin-internal-type'],
});
const obj = await client.get('my-plugin-internal-type', id);
Internal operation (no request)
const client = savedObjectsStart.getUnsafeInternalClient({
includedHiddenTypes: ['fleet-agent-policies'],
});
const result = await client.find({ type: 'fleet-agent-policies', perPage: 100 });
Paging through large result sets with createPointInTimeFinder
Use the point-in-time finder for server-side batch processing when you may exceed 1000 objects. Do not use it for request-response paging in multi-instance deployments (the finder is stateful).
const client = savedObjectsStart.getUnsafeInternalClient();
const finder = client.createPointInTimeFinder({
type: 'visualization',
perPage: 100,
});
try {
for await (const response of finder.find()) {
for (const obj of response.saved_objects) {
// process obj
}
}
} finally {
await finder.close();
}
Errors (e.g. not found, conflict) are thrown by the client. Use SavedObjectsErrorHelpers (e.g. isNotFoundError(error)) to classify and handle them.
createScopedRepository and createInternalRepository return an object that implements ISavedObjectsRepository (see src/core/packages/saved-objects/api-server/src/saved_objects_repository.ts). That interface exposes the same CRUD, find, search, resolve, bulk, point-in-time, and space/access-control methods as the client, plus a few repository-only methods. Use the repository only when you need those extra methods.
deleteByNamespace(namespace, options?)— Deletes all Saved Objects in the given namespace. Used when deleting a space. Not exposed on the client.incrementCounter(type, id, counterFields, options?)— Atomically increments one or more numeric fields; creates the document if it does not exist. Used for usage collection and metrics. Not exposed on the client. Options includeinitialize(set fields to 0 before incrementing) andupsertAttributes(attributes to set when creating the document).find(options, internalOptions?)— Same as the client’sfind, but accepts an optional second argumentSavedObjectsFindInternalOptions(e.g.disableExtensions) for internal callers that need to bypass extensions. Rarely needed by plugins.
- You need
incrementCounter(e.g. usage collection, dashboard or feature counters). - You need
deleteByNamespace(e.g. space deletion flow). - You have an advanced need for
findwith internal options (e.g. disabling extensions for a specific internal search).
Otherwise use the client.
Usage collection with incrementCounter
Use an internal repository so counters are not tied to a user request. Ensure usage collection is best-effort and does not harm stability or user experience.
// In a plugin's start hook or a service that has savedObjectsStart
const repository = savedObjectsStart.createInternalRepository(
['my-plugin-usage-type']
);
await repository.incrementCounter(
'my-plugin-usage-type',
'global',
['stats.apiCalls', 'stats.sampleDataInstalled'],
{ initialize: true }
);
await repository.incrementCounter('my-plugin-usage-type', 'global', ['stats.apiCalls']);
- if your usage type is hidden
Increment by a custom amount or with upsert
const repository = savedObjectsStart.createInternalRepository();
await repository.incrementCounter(
'dashboard_counter_type',
'counter_id',
[{ fieldName: 'stats.apiCalls', incrementBy: 4 }]
);
await repository.incrementCounter<{ appId: string }>(
'dashboard_counter_type',
'counter_id',
['stats.apiCalls'],
{ upsertAttributes: { appId: 'myId' } }
);
Deleting all objects in a space (deleteByNamespace)
Typically used by the Spaces plugin when a space is deleted. Use a scoped repository if the operation is tied to a request and authorization, or an internal repository for a system-level cleanup job.
const repository = savedObjectsStart.createInternalRepository();
const result = await repository.deleteByNamespace('space-id');
// result: { took, total, deleted, batches, ... }