← All articlesUmbraco

Umbraco migrations in practice: converting Angular block labels to UFM

How named migration plans and async steps work in Umbraco 17, using a targeted conversion of Block List and Block Grid labels as a practical example.

An application release sometimes needs to change existing data as well as deploy new code. Umbraco migrations give that change a named place in the upgrade process, with a recorded state so the application knows which transition to run next.

A useful example from a site upgrade I worked on is converting old AngularJS labels in Block List and Block Grid data type configurations to Umbraco Flavored Markdown, usually called UFM.

A plan is a sequence of states

A migration plan has a stable name and transitions between states. In Umbraco 17, an individual step can derive from AsyncMigrationBase and implement MigrateAsync. A small plan might look like this:

using Umbraco.Cms.Infrastructure.Migrations;

var plan = new MigrationPlan("Example.BlockLabels");
plan.From(string.Empty)
    .To<ConvertBlockLabels>("labels-to-ufm-v1");

ConvertBlockLabels is your migration class, not a built-in Umbraco step. The initial state here is the empty string. After the transition succeeds, the upgrader records the resulting state through Umbraco’s key-value service.

At the next startup, the recorded state tells the executor where to continue. Adding a later migration means adding a new transition from the previous state. Editing an already completed step does not automatically rerun it on existing installations.

Keep plan names and state identifiers stable across releases. They are part of the application’s upgrade history.

Run the plan at the appropriate point

In the implementation behind this example, an IAsyncComponent checks the runtime has reached RuntimeLevel.Run, builds the plan, and executes an Upgrader using injected services:

using Umbraco.Cms.Infrastructure.Migrations.Upgrade;

// Inside InitializeAsync, after the runtime-level check.
// These are constructor-injected services:
// IMigrationPlanExecutor migrationPlanExecutor
// ICoreScopeProvider scopeProvider
// IKeyValueService keyValueService
var upgrader = new Upgrader(plan);
await upgrader.ExecuteAsync(
    migrationPlanExecutor, scopeProvider, keyValueService);

This fragment shows the execution call rather than a complete composer. A package using Umbraco’s package-migration integration may have a different registration path. Do not register the same work through two mechanisms.

A migration is not a general-purpose background job. Keep startup work bounded and avoid remote API calls inside its database work. Long-running content processing may need a separately managed job with progress and resumability.

The problem: labels that no longer render

Older block labels could contain expressions such as:

Card: {{ heading }}

In UFM, the equivalent simple value expression is:

Card: {=heading}

The official UFM documentation covers value expressions, filters and more advanced rendering. The crucial point for a conversion is that an Angular expression is executable template logic; it is not always just a property alias with different brackets.

For example, custom Angular filters, ternaries and function calls need individual consideration. A broad string replacement can make them look converted while preserving none of their behaviour.

Make the transformation narrower than the data

The migration I worked on selects Block List and Block Grid data type configurations containing possible Angular expressions. It parses their JSON and inspects only label and labelTemplate string values, recursively through that configuration.

It does not rewrite every string in the JSON, and it does not alter the blocks’ content property values. Those boundaries make the change easier to review.

Here is a deliberately limited converter for simple property aliases:

using System.Text.RegularExpressions;

public static class SimpleBlockLabelConverter
{
    private static readonly Regex SimpleAlias = new(
        @"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}",
        RegexOptions.CultureInvariant,
        TimeSpan.FromSeconds(1));

    public static string Convert(string label) =>
        SimpleAlias.Replace(label,
            match => "{=" + match.Groups[1].Value + "}");
}

This helper is only the string transformation. The migration still needs JSON traversal, selection, persistence and logging. It leaves complex expressions unchanged; report remaining {{ tokens for manual review. A full converter can support a carefully tested subset of filters, but should not guess at an unknown expression.

The existing project implementation writes only changed configurations and logs converted labels, updated data types and unsupported expressions. Its direct database update is specific to the tested Umbraco schema and startup timing. It is not a recommendation to update core tables casually; consider service APIs, cache behaviour and the exact migration phase before choosing persistence.

Test the change and its second run

Useful converter checks include a plain alias, surrounding text, multiple expressions, an existing UFM label, an unsupported filter, an empty label and malformed JSON at the traversal boundary. Running the converter twice should produce the same result as running it once.

Then test the plan against a restored database. Verify labels in the backoffice, check unrelated data type settings, and restart the application to confirm the recorded state prevents the completed transition from executing again. Test failure handling too: state tracking does not make arbitrary external side effects or partially completed custom work safe to repeat.

Do not silently count unsupported labels as successful conversions. Keep a report so they can be reviewed even after the plan has advanced.

Keep schema files in step

If uSync or another deployment process manages data type configuration, update that source too. Otherwise a later import may restore the old Angular labels after the database migration has completed.

For a fresh installation, the current schema should already contain UFM labels. Existing installations need a forward path from their previous state. Test both cases rather than making a new site depend on replaying years of old conversions.

The same pattern can help introduce custom tables, seed required reference data or transform a known obsolete format. Choose a migration when the change belongs to a release, has a clear starting condition and can be verified. Keep a consistent backup: as Umbraco’s migration and downgrade guidance explains, changing a stored state is not the same as undoing the work.