Loading

Create a Saved Object type

This page describes how to define and register a Saved Object type (the type definition), not how to create Saved Object instances via the client. It covers registering the type, defining mappings and references, and defining the initial model version with full validation.

Saved object type definitions should live in a my_plugin/server/saved_objects directory.

The folder should contain one file per type (named after the snake_case type name) and an index.ts that exports all types.

import { SavedObjectsType } from 'src/core/server';

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  namespaceType: 'multiple-isolated',
  hidden: true,
  modelVersions: {
    1: modelVersion1,
    2: modelVersion2,
  },
  mappings: {
    dynamic: false,
    properties: {
      description: {
        type: 'text',
      },
      hits: {
        type: 'integer',
      },
    },
  },
  // ...other mandatory properties
};
		
  1. The name may form part of the URL path for the public Saved Objects HTTP API; use snake_case per our API URL path convention.
  2. This determines space behavior (single space, multiple spaces, or all spaces). See Sharing Saved Objects for details.
  3. If hidden: true, the type is not exposed by default via the Saved Objects Client APIs or HTTP APIs. Hidden types must be listed in SavedObjectsClientProviderOptions[includedHiddenTypes] to be accessible.
export { dashboardVisualization } from './dashboard_visualization';
export { dashboard } from './dashboard';
		
import { dashboard, dashboardVisualization } from './saved_objects';

export class MyPlugin implements Plugin {
  setup({ savedObjects }) {
    savedObjects.registerType(dashboard);
    savedObjects.registerType(dashboardVisualization);
  }
}
		

Each Saved Object type can define its own Elasticsearch field mappings. Because multiple types can share the same index, each type's mappings are nested under a top-level field that matches the type name.

Example for the search type:

search.ts

import { SavedObjectsType } from 'src/core/server';
// ... other imports
export const getSavedSearchObjectType: SavedObjectsType = {
  name: 'search',
  hidden: false,
  namespaceType: 'multiple-isolated',
  mappings: {
    dynamic: false,
    properties: {
      title: { type: 'text' },
      description: { type: 'text' },
    },
  },
  modelVersions: { ... },
  // ...other optional properties
};
		

This results in the following being applied to the .kibana_analytics index:

{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      ...
      "search": {
        "dynamic": false,
        "properties": {
          "title": {
            "type": "text",
          },
          "description": {
            "type": "text",
          },
        },
      }
    }
  }
}
		

Do not use field mappings like SQL column types. Treat them like SQL indexes: only add mappings for fields you need to search or query. Use dynamic: false at any level to allow Elasticsearch to store other fields without mapping them.

Never use enabled: false or index: false in your mappings. Elasticsearch does not support toggling these later, so you would not be able to query the data without migrating to a new field. Use dynamic: false instead, or omit the mapping for the field (it will still be stored, but not searchable or aggregatable).

What NOT to do:

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  ...
  mappings: {
    properties: {
      metadata: {
        enabled: false,
        properties: {
          created_by: { type: 'keyword' }
        }
      },
      description: {
        index: false,
        type: 'text'
      }
    }
  }
};
		
  1. Don't do this
  2. Don't do this

Preferred approach:

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  ...
  mappings: {
    properties: {
      dynamic: false,
      metadata: {
        properties: {
          // created_by can be stored but won't be queryable
        }
      },
    }
  }
};
		

This keeps all fields available for future querying if needed.

Elasticsearch has a default limit of 1000 fields per index, so add mappings carefully. Do not use dynamic: true on Saved Object types, as it can add an unbounded number of fields to the .kibana index.

Declare Saved Object references by adding id, type, and name to the references array.

router.get(
  { path: '/some-path', validate: false },
  async (context, req, res) => {
    const object = await context.core.savedObjects.client.create(
      'dashboard',
      {
        title: 'my dashboard',
        panels: [
          { visualization: 'vis1' }, [1]
        ],
        indexPattern: 'indexPattern1'
      },
      { references: [
          { id: '...', type: 'visualization', name: 'vis1' },
          { id: '...', type: 'index_pattern', name: 'indexPattern1' },
        ]
      }
    )
    ...
  }
);
		

[1] Store the reference name (e.g. vis1) in the attribute (e.g. dashboard.panels[0].visualization), not the id. That way the reference stays correct when the id in the references array is updated.

When you create a new Saved Object type, define an initial model version (version 1) that describes the current shape. The first version must be numbered 1, with no gaps in subsequent versions.

We recommend defining create and forwardCompatibility schemas that include all fields of your Saved Object type: those that appear in your mappings, as well as any non-indexed attributes you store. Exhaustive schemas give better validation on create and on read, and support safe rollbacks in Serverless.

Important

Changes to a type’s create schema, specifically adding new required fields, can effectively become breaking changes for consumers using the deprecated Saved Objects HTTP CRUD APIs, because those APIs validate create and bulk-create payloads against this schema for that type.

import { schema } from '@kbn/config-schema';
...
const schemaV1 = schema.object({
  foo: schema.string(),
  bars: schema.arrayOf(schema.string()),
});
...
const myType: SavedObjectsType = {
  ...
  modelVersions: {
    1: {
      changes: [],
      schemas: {
        create: schemaV1,
        forwardCompatibility: schemaV1.extends({}, { unknowns: 'ignore' }),
      },
    },
  },
  ...
};
		

If you are working with complex Saved Object types that have many properties—especially if these properties are defined dynamically or if you use external validation libraries like zod—you may provide partial schemas for the create and forwardCompatibility fields. At a minimum, you must include all properties defined in your ES mappings to help ensure data consistency, since these mappings are stored in the Elasticsearch index. In these situations, your partial schemas can be defined as shown below:

import { schema } from '@kbn/config-schema';
...
const schemaV1 = schema.object({
  mappedProperty1: schema.string(),
  mappedProperty2: schema.arrayOf(schema.string()),
  ...
  mappedPropertyN: schema.boolean(),
});
...
const myType: SavedObjectsType = {
  ...
  modelVersions: {
    1: {
      changes: [],
      schemas: {
        create: schemaV1.extends({}, { unknowns: 'allow' }),
        forwardCompatibility: schemaV1.extends({}, { unknowns: 'allow' }),
      },
    },
  },
  ...
};
		

You have two options, depending on how much control you need:

  • Full control — Set your type as hidden: true. You handle all HTTP APIs and access; clients must explicitly include the type (e.g. via includedHiddenTypes).
  • Use client, custom HTTP — Set hiddenFromHttpApis: true (and hidden: false). The type is available to the Saved Objects client as usual, but not exposed by the global Saved Objects HTTP APIs, so you can implement your own routes.
import { SavedObjectsType } from 'src/core/server';

export const foo: SavedObjectsType = {
  name: 'foo',
  hidden: false, [1]
  hiddenFromHttpApis: true, [2]
  namespaceType: 'multiple-isolated',
  mappings: { ... },
  modelVersions: { ... },
  ...
};
		

[1] Must be false to use hiddenFromHttpApis. [2] Set to true to keep using the client while building your own HTTP API.