Concepts

Markdown

Schema changes

Renaming fields, changing field types, or toggling the translatable flag on collections with existing content requires careful handling to avoid data loss. This page documents renames from externa-core 1.2.1+, type / translatable locks from 1.3.0+, the repair command for older drafts, and safe field conversion with dry run from 1.4.0 (core#220).

Upgrading from 1.2.0 or earlier

Field renames are safe from externa-core 1.2.1 (#218, #223). If you renamed fields on an older version, run the repair command in Repairing older drafts right after upgrading, before the scheduler runs or anyone publishes.


Renaming fields

Current behaviour

Field values are stored internally by field ID, not by name (app/Services/Collections/CollectionItemValuesWriter.php in externa-core). This means:

  • Rename without pending drafts: Content is preserved. The published API immediately reflects the new field name.
  • Rename with pending drafts (versioned collections): From 1.2.1, pending drafts and older revisions keep the renamed field's value, so publishing (manual or scheduled) and restoring revisions preserve content. On 1.2.0 and earlier, the value was dropped because drafts and revisions stored a snapshot keyed by the old field name.

Fixed in v1.2.1 (#218, #223).

Repairing older drafts

Drafts and revisions created before 1.2.1 may still hold a pre-rename field key. Right after deploying 1.2.1, before the scheduler runs or anyone publishes:

  1. Dry run (default, no writes): php artisan externa:repair-renamed-field-drafts
  2. Apply: php artisan externa:repair-renamed-field-drafts --apply

Mappings come from the CollectionField activity-log rename history. For orphaned keys with no history, pass --map=collection:old=new (collection slug or id; repeatable). The command never overwrites a value already stored under the new key, and publish-time behaviour is unchanged. Keys reported as unmappable belong to deleted fields and can be ignored.

If values were already lost by publishing on an older version, restore from a database backup taken before the rename, or re-enter them via the admin UI or Collections API.

API breaking change

Renaming a field is a breaking change for API consumers. The Public CMS API, Collections API, and GraphQL all use the current field name as the key. Update your frontend or client code after renaming.


Changing field types

Current behaviour (core 1.3.0+)

Externa rejects type changes on a field that already has stored content (published values, draft_data, or values on soft-deleted items) — core#219 / PR #265. The admin form returns a 422 validation error and locks the type control (the field list exposes has_stored_values); the AI assistant’s ManageCollections.updateField returns an error. Re-submitting the same type, or changing an empty field, still works. Stored values are never converted by the field form. From 1.4.0, use field conversion for the supported pairs, or the manual path below for anything else.

Safe path

  1. Create a new field with the desired type.
  2. Migrate content from the old field to the new field:
    • Export the collection, transform the values, and re-import; or
    • Use the Collections API or Admin API to update each item programmatically.
  3. Delete the old field after verifying the migration.

Toggling the translatable flag

Current behaviour (core 1.3.0+)

Externa rejects translatable changes on a field that already has stored content (published values, draft_data, or values on soft-deleted items) — same lock as type changes (PR #265). The admin form returns a 422 validation error and locks the Translatable checkbox (the field list exposes has_stored_values); the AI assistant’s ManageCollections.updateField returns an error. Re-submitting the same flag, or changing an empty field, still works. Stored values are never converted by the field form. From 1.4.0, field conversion flips the flag with a locale map or a picked locale; otherwise use the manual path below.

Safe path

Follow the same migration strategy as type changes above:

  1. Create a new field with the desired translatable setting.
  2. Migrate and transform values (flatten or expand locale maps as needed).
  3. Delete the old field.

Field conversion (core 1.4.0)

From externa-core 1.4.0 (#220), fields with stored content can change type or the translatable flag with a migration of every value, always previewed first.

Supported conversions

FromToTranslatable
string / textarea / markdownstring / textarea / markdownunchanged or flipped
stringselectunchanged or flipped; values map onto options (missing options can be created)
any supported typesame typefalse → true (value moves to the default content locale) or true → false (pick the locale to keep)

Print the matrix from core with php artisan externa:field-convert --matrix. Other pairs are rejected — use the manual safe path.

Admin

Fields page → field menu → Change type… opens the convert dialog. It lists allowed targets (GET /collections/{collection}/fields/{field}/convert-targets), runs a Dry run with ok / failed / drafts / skipped counts, then Convert (POST …/fields/{field}/convert with type, translatable, locale, create_missing_options, option_mapping, dry_run). Applying from the dialog queues ConvertCollectionFieldJob (a queue worker must run) unless sync=true is sent. Needs can-edit-collections.

CLI: externa:field-convert

# Preview (no writes)
php artisan externa:field-convert 42 --to=textarea --dry-run

# Flip to non-translatable, keep the Italian value
php artisan externa:field-convert 42 --translatable=0 --locale=it --dry-run
php artisan externa:field-convert 42 --translatable=0 --locale=it
OptionMeaning
fieldField id (required)
--to=Target type; omit to keep the current type
--translatable=0 / 1; omit to keep the current flag
--locale=Locale to keep when collapsing translatable → non-translatable
--create-missing-options=1Create select options from values (string → select)
--chunk=100Items per write batch
--dry-runPreview only
--matrixPrint the supported matrix and exit

The summary prints ok, unchanged, failed, skipped, and with_draft counts. Without --dry-run, the command applies the conversion; there is no confirmation prompt, so run the dry run first.

Guarantees

  • All or nothing: if any published or draft value cannot be converted, nothing is written (Conversion aborted: N value(s) cannot be converted. Nothing was written.).
  • Published values and pending drafts are converted, in chunked transactions.
  • Each changed item gets a revision with source=convert, and the change is written to the activity log.
  • Historical revisions stay as they were. Restoring an old revision adapts its value to the current type, or rejects the restore with a validation error when the value no longer fits.
  • The AI assistant can call ManageCollections convert_field; it is a write action, so it needs the AI write gate and an approval.

Renaming is still a breaking change for API consumers, and so is a type change: update clients that read the field.