Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

Straight answer: the Metadata namespace lets Apex read and deploy a small set of metadata types, with custom metadata type records and page layouts being the ones people actually use. Deployments are asynchronous and report back through a callback, so nothing you enqueue is visible in the same transaction. It is justified when an admin or an installer needs to change configuration through your UI rather than through a deployment. It is not a substitute for source control, and using it that way ends badly.

Why this exists at all

Custom metadata type records cannot be created or updated with DML. They are metadata, not data, so insert new My_Setting__mdt(...) does not compile against a real deployment path. That is the single most common reason developers find this API: they built a configuration UI on custom metadata and then discovered they could not save from Apex.

The other common reason is a post install script that adds a section to a page layout, or a wizard that configures the org for the customer.

The shape of a deployment

Metadata.CustomMetadata rec = new Metadata.CustomMetadata();
rec.fullName = 'Integration_Setting__mdt.Prod_Endpoint';
rec.label    = 'Prod Endpoint';

Metadata.CustomMetadataValue val = new Metadata.CustomMetadataValue();
val.field = 'Endpoint__c';
val.value = 'https://api.example.com/v2';
rec.values.add(val);

Metadata.DeployContainer container = new Metadata.DeployContainer();
container.addMetadata(rec);

Id jobId = Metadata.Operations.enqueueDeployment(
    container, new DeployCallbackHandler()
);

The fullName is the object API name, a dot, and the record’s developer name. Get that wrong and the deployment fails asynchronously with a message you will only see in the callback.

The callback implements Metadata.DeployCallback:

public class DeployCallbackHandler implements Metadata.DeployCallback {
    public void handleResult(
        Metadata.DeployResult result,
        Metadata.DeployCallbackContext context
    ) {
        if (result.status == Metadata.DeployStatus.Succeeded) {
            return;
        }
        for (Metadata.DeployMessage m : result.details.componentFailures) {
            ErrorLogger.publish('Metadata deploy failed: ' +
                m.fullName + ' ' + m.problem);
        }
    }
}

Handle the failure branch properly. A deployment that quietly fails leaves your configuration screen showing values the org does not have.

The asynchronous part, and what it costs you

enqueueDeployment returns a job id immediately. The deployment happens afterwards, outside your transaction. Three consequences follow.

You cannot read back what you just wrote. A query for the custom metadata record right after enqueuing returns the old value. If your UI needs to show the new state, show what the user entered and refresh later, or poll the deploy status.

Errors arrive late. Validation happens during the deployment, not at enqueue time. The user has already seen a success message by then.

Tests do not deploy. The deployment does not run in a test context, so the callback is not invoked for you. Test the code that builds the container, test the callback by constructing a Metadata.DeployResult and calling handleResult directly, and put enqueueDeployment behind a thin wrapper you can stub.

Retrieval works too, through Metadata.Operations.retrieve, which pulls the current definition so you modify it rather than rebuild it. For page layouts that is essential: you want to add one field to the layout that already exists, not construct one from nothing.

When it is justified

  • A configuration UI over custom metadata. An admin facing screen that writes settings is the textbook case, and there is no other way to do it.
  • Managed package install and upgrade scripts. Seeding default configuration records that the subscriber then edits.
  • A setup wizard that has to touch layouts or record types as part of enabling a feature.

When it is not

Do not use it as a deployment mechanism for things that belong in source control. Metadata that changes because Apex decided to change it has no commit, no reviewer and no history, and the next person to compare a sandbox against the repository will be baffled by the difference.

Do not use it per record or per request. Deployments are heavy and asynchronous, and one on every save produces a queue of jobs and a lot of confusion.

And do not reach for it when a custom setting would do. Hierarchy custom settings are plain DML, immediately readable, and support per user and per profile values. Custom metadata is the right choice when configuration should be deployable and packageable. Custom settings are the right choice when it is runtime state.

Beyond the Metadata namespace

The supported type list is short. For anything outside it, the route is an HTTP callout to the Metadata or Tooling API, which means a named credential, an authenticated integration user and a good reason. That is a project, not a convenience. The usual right answer is a deployment.

Frequently asked

Can I create a custom field from Apex? Not through the Metadata namespace. You would need a Metadata API callout, and creating schema at runtime is a decision to think very hard about.

Does the deployment respect the running user‘s permissions? It runs with the permissions of the user who enqueued it, and modifying metadata generally requires Customize Application. A standard user pressing your save button will get a failure in the callback.

Can I deploy several components at once? Yes. Add them to the same DeployContainer and they succeed or fail together.

How do I check status later? Query DeployRequest through the Tooling API using the job id, or record the id yourself and let the callback update your own status record.