---
id: kibDevTutorialSavedObject
slug: /kibana-dev-docs/tutorials/saved-objects
title: Register a new saved object type
description: Learn how to register a new saved object type.
date: 2021-02-05
tags: ['kibana','onboarding', 'dev', 'architecture', 'tutorials']
---

Saved Object type definitions should be defined in their own `my_plugin/server/saved_objects` directory.

The folder should contain a file per type, named after the snake_case name of the type, and an index.ts file exporting all the types.

**src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts**

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

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization', [1]
  hidden: true, [3]
  switchToModelVersionAt: '8.10.0', // this is the default, feel free to omit it unless you intend to switch to using model versions before 8.10.0
  namespaceType: 'multiple-isolated', [2]
  mappings: {
    dynamic: false,
    properties: {
      description: {
        type: 'text',
      },
      hits: {
        type: 'integer',
      },
    },
  },
  modelVersions: {
    1: dashboardVisualizationModelVersionV1,
    2: dashboardVisualizationModelVersionV2,
  },
};
```

[1] Since the name of a Saved Object type forms part of the URL path for the public Saved Objects HTTP API,
these should follow our API URL path convention and always be written in snake case.

[2] This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means
that objects of this type can only exist in a single space. See
<DocLink id="kibDevDocsSavedObjectsIntro" section="sharing-saved-objects" text="Sharing Saved Objects"/> for more information.

[3] This field determines whether repositories have access to the type by default. Hidden types will not be automatically exposed via the Saved Objects Client APIs.
Hidden types must be listed in `SavedObjectsClientProviderOptions[includedHiddenTypes]` to be accessible by the client.

**src/plugins/my_plugin/server/saved_objects/index.ts**

```ts
export { dashboardVisualization } from './dashboard_visualization';
export { dashboard } from './dashboard';
```

**src/plugins/my_plugin/server/plugin.ts**

```ts
import { dashboard, dashboardVisualization } from './saved_objects';

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

## Mappings

Each Saved Object type can define its own Elasticsearch field mappings. Because multiple Saved Object
types can share the same index, mappings defined by a type will be nested under a top-level field that matches the type name.

For example, the mappings defined by the dashboard_visualization Saved Object type:

**src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts**

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

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  ...
  mappings: {
    properties: {
      dynamic: false,
      description: {
        type: 'text',
      },
      hits: {
        type: 'integer',
      },
    },
  },
  modelVersions: { ... },
};
```

Will result in the following mappings being applied to the .kibana index:

```ts
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      ...
      "dashboard_vizualization": {
        "dynamic": false,
        "properties": {
          "description": {
            "type": "text",
          },
          "hits": {
            "type": "integer",
          },
        },
      }
    }
  }
}
```
Do not use field mappings like you would use data types for the columns of a SQL database. Instead, field mappings are analogous to a
SQL index. Only specify field mappings for the fields you wish to search on or query. By specifying `dynamic: false`
 in any level of your mappings, Elasticsearch will accept and store any other fields even if they are not specified in your mappings.

Never use `enabled: false` or `index: false` in your mappings. Elasticsearch does not support toggling these mapping options, so if 
your plugin ever needs to query the data, you will not be able to do so. Since these fields cannot be queried, they would require 
migrating to a new field and making associated code changes. Instead, use `dynamic: false` which provides the same flexibility while
maintaining the future ability to query fields if necessary.

Here's an example of what NOT to do:

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

Instead, use `dynamic: false` if you want to persist data which does not need to be queryable.
```ts
export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  ...
  mappings: {
    properties: {
      dynamic: false,  // ✅ Do this instead
      metadata: {
        // dynamic: false gets inherited from above
        properties: {
          // `created_by` can now be stored but won't be queryable
        }
      },
      // `description` can now be stored but won't be queryable
    }
  }
};
```

This approach maintains flexibility while ensuring all fields remain queryable if needed in the future.

Since Elasticsearch has a default limit of 1000 fields per index, plugins should carefully consider the
fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary
 amount of fields to be added to the .kibana index.

## References

Declare <DocLink id="kibDevDocsSavedObjectsIntro" section="References" text="Saved Object references"/> by adding an id, type and name to the
 `references` array.

```ts
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] Note how `dashboard.panels[0].visualization` stores the name property of the reference (not the id directly) to be able to uniquely
identify this reference. This guarantees that the id the reference points to always remains up to date. If a
 visualization id was directly stored in `dashboard.panels[0].visualization` there is a risk that this id gets updated without
 updating the reference in the references array.

## Migrations

Saved Objects support schema changes between Kibana versions, which we call migrations, implemented with model versions.
Model version transitions are applied when a Kibana installation is upgraded from one version to a newer version, when exports are imported via
  the Saved Objects Management UI, or when a new object is created via the HTTP API.

### Defining model versions

Model versions are bound to a given [savedObject type](https://github.com/elastic/kibana/blob/9b330e493216e8dde3166451e4714966f63f5ab7/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_type.ts#L22-L27)

When registering a SO type, a [modelVersions](https://github.com/elastic/kibana/blob/9a6a2ccdff619f827b31c40dd9ed30cb27203da7/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_type.ts#L138-L177)
property is available. This attribute is a map of version numbers to [SavedObjectsModelVersion](https://github.com/elastic/kibana/blob/9a6a2ccdff619f827b31c40dd9ed30cb27203da7/packages/core/saved-objects/core-saved-objects-server/src/model_version/model_version.ts#L12-L20)
which is the top-level type/container to define model versions.

The modelVersion map is of the form `{ [version: number] => versionDefinition }`, using single integer to identify a version definition.

The first version must be numbered as version 1, incrementing by one for each new version.

```ts
import { schema } from '@kbn/config-schema';
import { SavedObjectsType } from 'src/core/server';


const schemaV1 = schema.object({ title: schema.string({ maxLength: 50, minLength: 1 }) });
const schemaV2 = schemaV1.extends({
  description: schema.maybe(schema.string({ maxLength: 200, minLength: 1 })),
});

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  ...
  mappings: {
    dynamic: false,
    properties: {
      title: { type: 'text' }, // This mapping was added before model versions
      description: { type: 'text' }, // mappings introduced in v2
    },
  },
  modelVersions: {
    1: {
      // Sometimes no changes are needed in the initial version, but you may have
      // pre-existing mappings or data that must be transformed in some way
      // In this case, title already has mappings defined.
      changes: [],
      schemas: {
        // The forward compatible schema should allow any future versions of
        // this SO to be converted to this version, since we are using
        // @kbn/config-schema we opt-in to unknowns to allow the schema to
        // successfully "downgrade" future SOs to this version.
        forwardCompatibility: schemaV1.extends({}, { unknowns: 'ignore' }),
        create: schemaV1,
      },
    },
    2: {
      changes: [
          // In this second version we added new mappings for the description field.
          {
            type: 'mappings_addition',
            addedMappings: {
              description: { type: 'keyword' },
            },
          },
          {
            type: 'data_backfill',
            backfillFn: (doc) => {
              return {
                attributes: {
                  description: 'my default description',
                },
              };
            },
          },
      ],
      schemas: {
        forwardCompatibility: schemaV2.extends({}, { unknowns: 'ignore' }),
        create: schemaV2,
      },
    },
  },
};
```

That way:
- SO type versions are decoupled from stack versioning
- SO type versions are independent between types

### Saved object migrations on serverless

On serverless, Kibana cannot have any downtime &mdash; even during data migrations. In order to accomplish this we rollout new versions while old versions are still running. As soon as new versions are ready to start serving, requests will be directed to them.

<DocCallOut title="Only once a new version is done being rolled out will data migrations be run.">
</DocCallOut>



This has a few implications:

1. A _new version_ of application code should **never** introduce a new Saved Object field and treat it as a **required** field
2. A _new version_ of application code must be **fully backward compatible** with the **previous version's (n-1) Saved Object fields**

In order to introduce a new, required Saved Object field the following algorithm _must_ be followed:

1. Introduce a new model version field, consider this field _optional_ in any application code that uses it
2. Provide a `data_backfill` function for the new field
3. Merge to `main`
4. Wait for the next serverless release containing your data migration changes to complete
5. Update your code marking the new field as required in interfaces
6. Merge to `main`

At step 6 your code that was just merged to `main` will be guaranteed to find a value for the new field in Saved Objects.

<DocCallOut color="warning" title="What happens when you skip step 4?">
  Not waiting until your `data_backfill` has been released means that none of your SO documents will have the field populated with the value you provided in the `data_backfill` function. The new field value will be `undefined` until migrations have run which only happens _after_ application code has already started running!
</DocCallOut>


### Testing model versions
Bugs in model version transitions cause downtime for our users and therefore have a very high impact. Follow the <DocLink id="kibDevTutorialTestingPlugins" section="saved-objects-model-versions" text="Saved Objects model versions"/> section in the plugin testing guide.

### How to opt-out of the global savedObjects APIs?

There are 2 options, depending on the amount of flexibility you need:
For complete control over your HTTP APIs and custom handling, declare your type as `hidden`, as shown in the example.
The other option that allows you to build your own HTTP APIs and still use the client as-is is to declare your type as hidden from the global saved objects HTTP APIs as `hiddenFromHttpApis: true`

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

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

[1] Needs to be `false` to use the `hiddenFromHttpApis` option

[2] Set this to `true` to build your own HTTP API and have complete control over the route handler.
