Metadata scaffolding
Creating objects, attributes, routes and forms through the engine instead of hand-writing yaml – and the code templates.
Writing yaml by hand means remembering a UUID, the property names of a kind and where a form is registered. The engine does that part, and the same operations are available from the CLI, an editor and an agent.
Metadata scaffolding
The toolkit takes over the metadata mechanics: UUIDs, indentation, precise yaml insertions,
duplicate checks and the compatibility of a section with a kind. The same operations show up in
three places. In the CLI they are subcommands with JSON output. In MCP they are the meta_*
tools for agents. In LSP they are the custom xbsl/meta* requests behind the VS Code metadata
tree.
33 kinds of project element can be created: from Catalog and Document to VirtualTable
paired with its mandatory .xbql query, ScheduledJob, contracts, rights and commands. Each
kind carries what the docs make mandatory. That means the platform’s own default scope,
InSubsystem - widen it deliberately with --scope. It also means a module stub for the handler
the kind cannot live without, and a note about whatever the generator must not invent for you.
Kinds whose content is drawn in the designer, ReportPanel and IntegrationProcess, are
deliberately absent.
xbsl new-project . vendor App # descriptor + module + a subsystem
xbsl new-object <subsystem-dir> <kind> <name> # kind: Catalog, Document, Enum, ... (--help spells them)
xbsl new-object <subsystem-dir> <kind> <name> --presentation <plural> --object-presentation <singular>
xbsl add-field <object>.yaml <item-kind> <name> --type <type> # item kind: реквизит, ... (--help lists them)
xbsl add-form . --name <object> # object + list forms, registered
xbsl add-form . --name <object> --forms list-cards # list form as a card grid
xbsl new-object <subsystem-dir> <http-service-kind> <name> --routes "GET /, POST /, GET /{id}"
xbsl add-route <service>.yaml "DELETE /{id}" # url template + handler stub
xbsl add-method <module>.xbsl <method> --annotations <annotation> --after <existing-method>
xbsl set-localization <strings>.yaml <key> --value <language>=<text> ... # one row, every language
xbsl add-subsystem vendor/App <name>
xbsl add-dependency . acme CurrencyConverter 2.0 # attach a library to the project
xbsl rename-object . <old-name> <new-name> # rename files + update references
xbsl delete-object . --name <object> # the plan; --apply deletes and lists leftovers
xbsl move-object . <object>.yaml <package-dir> # into a package: files + imports + full names
xbsl rename-package . <package-dir> <new-name> # the folder + imports + full names
xbsl move-resource . <resource> <folder> # into another folder: files + keys
xbsl rename-resource-folder . <folder> <new-name> # the folder + the keys of its files
xbsl delete-resource-folder . <folder> # the plan; --apply deletes and lists references
xbsl resource-references . <resource> # the places that name a resource
xbsl set-access . --name <object> --default <access-method>
xbsl object-info . --name <object> # fields, tabulars, forms, namespace
xbsl project-info . # projects, subsystems, objects by kind
xbsl project-info . --package <package> # the objects of one package
The object kind is taken in either spelling, and xbsl new-object --help lists the kinds a
project can hold. The item kind of add-field and set-field-property is a word of the tool
itself: реквизит, табличная-часть, строка and the rest that xbsl add-field --help lists.
Each has an English twin for a project written in English, the platform’s own term for the same
item - attribute, tabular-part, string - and case and hyphens do not matter. The access
methods and rights of set-access are taken in either language too, the way a yaml file of that
language spells them: PermitAuthenticated or its Russian name, Read or Чтение. The
annotations and every identifier reach the CLI in the spelling the platform uses for the
project’s development language. The prose of this page names them by their English equivalents -
Catalog, Attributes, OnServer - which is why the examples above use placeholders.
--presentation writes the caption of the element where its kind keeps it. A catalog, a
document, an exchange plan and a settings storage keep two captions in the Interface section:
--presentation is the list caption, in the plural, and --object-presentation the object
caption, in the singular (MCP presentation and object_presentation). The tool cannot derive
one from the other, and naming/presentation asks for both, so an object created with both lints
clean. Any other kind refuses --object-presentation. An information register pairs its list
caption with the record one, --record-presentation (MCP record_presentation), which no other
kind takes. A constants set has one caption, the record one, and --presentation writes it. So
does a processing: its caption belongs to the Interface section itself. --periodicity (MCP
periodicity) makes a constants set periodic - Day, Month, Quarter or Year - and a
periodic set has a list beside its record, so it is captioned the way an information register
is: --presentation for the list, --record-presentation for the record.
A property the kind is born with goes in only where the compatibility mode of the project knows
it. An access key is born computed (ManualGrant: False, with the stub of its handler): a project
in the newest mode refuses a key that does not choose its flavour, and a mode that predates the
property gets the key without the line and a note saying why.
project-info answers narrowly. --kind, --subsystem and --package narrow the list of objects,
--brief leaves the lists out and keeps the counts, and --project walks only the named project -
by Name, Vendor::Name or its folder, since a repository root often holds examples beside the
project. Every object carries its subsystem, its package (empty at the subsystem root, Batches
or Batches::Archive inside one) and the full namespace, Vendor::Project::Subsystem::Package -
the prefix the generated forms use for their row types. packages lists the packages of the listed
objects with their folders. The object kinds, the section kinds and the access methods come with
--reference or --brief: they do not depend on the sources.
The sources themselves may be written in either language, and the scaffolding reads both. An
object whose file spells its kind and its sections in English - ElementKind: Catalog,
Attributes: - is found, described and edited exactly like one spelled the other way; see
demo-en/ for such a project. What the tool writes follows the file it writes into: a new
attribute of an English object gets Name: and Type:, a missing section is created as
Attributes:, a subsystem and a library entry are spelled like the project around them. The
pairs come from the platform data, from the metamodel’s own English name of every property. So a
name the platform spells differently depending on the class keeps its original spelling in both
reading and writing, with nothing left to guess: the enumeration values section is Items there
and Elements elsewhere. In a value, the names of the author stay as given and the words of the
platform follow the file: the platform types of a type expression, an access method
(PermitEveryone in an English file, its Russian name in a Russian one). A boolean an MCP call
passes as true or false is written in the word of the file’s language: Истина and Ложь
in a Russian file, True and False in an English one, as the sources of the distribution
write it.
add-field puts a new item at the end of the section of its kind, and creates that section only
when the file has none. A register keeps its data in Dimensions and Resources. So an
attribute asked of a register that holds resources and no attributes lands in a new Attributes
section at the end of the file. The notes field says so and names the kind that would have
placed the field beside the existing ones: a resource here, and the other way round for a
resource asked where only attributes exist.
An interface component takes its own properties the same way. The property kind writes a name and
a type into Properties, with no Id: the class of a component property has none. --prop adds
the three keys the documentation lists for such a property: DefaultValue, StoredData and
Contextual (--prop DefaultValue=0). A component without the section gets it where the designer
writes it, after Inherits and in front of Events. The event kind fills the component’s own
Events the same way: a name and the type of the event object, ComponentEvent when --type is
not given, or EventWithData<String> and the like for an event that carries a value. A missing
Events section goes right after Properties, or at the end of the file when the component has
no properties. --doc gives an item a description, and the text becomes its documentation
comment: the ## lines at the head of the item, the only comment the development environment
keeps when it writes the file out.
Forms are generated with real content: input fields per attribute, including the standard
Name, Number and Date fields and hierarchy support, DynamicList columns, TabularParts
tables and a report form with parameters. The form is registered in the Interface section of
its owner. --dry-run prints the changes with full file texts and writes nothing. That is the
flag the VS Code extension uses: it applies the changes itself, through its own undo-friendly
edits.
The captions of the form and of its columns go through the project’s dictionary. When the
subsystem folder holds one LocalizedStrings element and the descriptor declares two
localization languages, a caption is written as $Dictionary.Name. The keys those references
need join the dictionary, and the translations it already has, in the same operation: a reference
to a key nobody declares fails the apply. Without such a dictionary the caption stays a
literal.
--forms list-cards builds the list form as a card grid instead of a table: a CustomList
whose RowsContainer is a MatrixGroup, plus a generated ListRow<Name> row component named
after the object. The card takes a Title, a photo and up to three more fields, with dates
formatted. An attribute of type BinaryObject.Reference switches the card to CustomCard, with
the image above the caption. The notes field reports what landed on the card and what did not.
--card-min-width sets the grid column width, 400 by default and 250 with a photo, and
--card-placeholder sets the image shown when the photo is empty.
set-localization writes one localized string into every language at once. The text of the
default language goes into the LocalizedStrings element itself, which is where the platform
keeps it; every other language gets its own Localization/<Code>/<Name>.yaml. This was the
missing half of add-localization, which adds a language. A caption used to be typed into the
element and again into its English twin, the two files drifted apart, and nothing but a pair of
eyes could compare them. A language named without a translation file is refused: add the language
first. An existing language the call says nothing about still gets the row, with the default text
and a note, so no translation is left a key short. A key keeps the section it already lives in,
and a new one goes to Rows unless --section says Templates.
--dry-run here is unlike the rest of the family: a summary by row (key, language, file, the
text before and after) rather than the whole files, because one key on a two-language project
used to print over 100 KB to report a one-line change. --full-text asks for the files back
next to the summary. --entries-file <json-or-yaml> writes many keys in one call - {Key: {Language: Text}} - and --entry Key={"Language":"Text"} adds one more on the command line;
either combines with a single name argument (--value belongs to that argument and is
refused without it). The batch file is read with every scalar left as text, so Yes, No, On
and 12:30 stay the captions they look like instead of becoming booleans and a number, and a
value that is not text is refused by the key that holds it. A file that several keys touch is read once and
written once regardless of how many of them land in it, and the whole batch is planned before
any of it is applied: an invalid key refuses the call before a single file is written, rather
than leaving the ones before it already on disk. A translation the call only read - every key it
holds already carrying that very text - keeps its bytes and its mtime and stays out of the files
it reports. MCP meta_set_localization takes the same
entries, dry_run and full_text.
add-field <strings>.yaml строка <key> (meta_add_field with the kind строка) adds the key,
and --type carries its text in the default language - the key itself when omitted. The key
lands in the element and is echoed into the translations that already exist, with the
default-language text, so none of them is left a key short. The note names set-localization,
which is what writes the text of a translation. A call on the translation file itself is
refused, because that file carries neither a kind nor the sections of an element. The refusal
names the element the file belongs to and the same set-localization. It used to read “the
kind ? has no section for it”.
add-localization <strings>.yaml <language> adds a translation, and localization-info lists
the languages one can be added for. The languages are the platform’s own enumeration, read
from the data together with the compatibility mode each one needs, so Chinese, Vietnamese and
Tajik stand next to Russian and English once the data carries them. A language a newer mode
added is offered and added only for a project of that mode, and a value of
LocalizationLanguages the data knows no language by is named in the notes instead of
vanishing from the answer. A new translation goes to the folder of the language code with a
capital (Localization/Vi); a folder already written in another case (vi) is found as it
is, since the platform reads the code regardless of case.
add-dependency attaches a library: it writes the Libraries section of the project descriptor
with Name, Vendor and Version. The version here is the library’s release version. A
release is issued in the control panel, and a build version with a suffix such as 1.0-42 is
rejected. Different versions of one library within a project are not allowed, so attaching a
library that is already attached updates the version of the existing entry. What is attached now
shows up in project-info under projects[].libraries. The vendor, name, version and the
qualified type names of a library come from parsing its archive:
elemctl inspect <file.xlib>.
set-access edits AccessControl.Permissions of an object in place, and it knows what each kind
allows. --default sets the Default right. --permission Read=PermitEveryone sets an
individual one, custom rights of a PrivilegeOnElement included. Either language is taken, and
the file gets its own: an English object is written Read: PermitEveryone even when the call
spoke Russian. --calc-by fills
ComputePermissionsBy, which is mandatory for PermissionsComputedForEachObject. Wrong methods,
rights a kind does not have, and per-object rights on a ConstantsSet, a service or a
Processing are all rejected: their access settings have no ComputePermissionsBy. The
computed-permission handlers stay yours to write, and the notes field says which ones you need.
object-info reports the current permissions and the kind’s rights, project-info the Default
of every object, both in the Russian names whichever language the file is written in. No section
there means the platform falls back to PermitAdmins.
rename-object renames the object’s files, including its forms, the generated ListRow<Name>
component of a card list and the WSDL descriptions of a SOAP service client. A description keeps its
number, and a reference from one description to another by file name gets the new name.
Imported XML schemas (<Name>.Wsdl.<N>.xsd) move, rename and delete with the client too;
local location and schemaLocation links follow a rename, while external URLs stay unchanged. The command
rewrites references across the whole project and with an eye on the context: the reference-bearing
yaml keys Type, Table, DataSource, Form and FormType, the = bindings and the .xbsl code.
Attributes, components or dynamic-list fields that merely share the old name are left alone, and so
are string literals with UI text. Comments in modules and in yaml get the new name whole, quotes
inside a comment included. The translation dictionary is not rewritten as a source: for every
comment line the rename changed, its phrases translation is written again under the new line,
and the old key stays for any text that still reads it. tokens are left as they are, because the
old name may live on in a namesake, and the notes name the pairs: the English spelling of the new
name is yours to set. --new-presentation and --old-presentation update the Title
and Presentation of the object and its forms. The object’s Id is untouched, so the platform
keeps the stored data.
delete-object deletes an object whole: the yaml and module pair, its forms and the generated
ListRow<Name> row component with their pairs, and the WSDL descriptions of a SOAP service client.
A subsystem is the folder the files live in, so the membership goes away with them. Every remaining
mention of the name across the project is listed by file and line, string literals and comments
included. A router opening a form by a name in a string, or code seeding data, is exactly the
leftover that otherwise surfaces as a runtime error. The command does not edit those mentions: which
one is dead code is the author’s call. Deletion is irreversible, so without --apply the command
prints the plan. The MCP tool meta_delete_object answers with a plan too, since its dry_run
defaults to true.
A rename that only changes letter case, Goods into goods, runs in two steps through a
temporary name. A case-insensitive filesystem, Windows or macOS, addresses the old and the new
name as one file, and a single-step rename between them is not guaranteed. A failure of the
second step undoes the first and leaves no temporary name on disk. On a case-sensitive filesystem
the name is free and the rename runs in a single step. The tool renames the files itself, but
check the version control system. Git on a case-insensitive filesystem folds ASCII letters only,
so a Latin rename goes unnoticed: record it explicitly with git mv <old> <new>. A Cyrillic one
is recorded as a delete plus an add, and then every other clone on such a filesystem stops at
“untracked working tree files would be overwritten by merge”. There the file under the old name
has to be deleted before pulling.
move-object (MCP meta_move_object) moves an object into another folder of its project: a package
of the subsystem - a folder that does not exist yet becomes one - another package, the subsystem
root or another subsystem. The object takes its forms, modules, list row and list table along, and a
SOAP service client its WSDL descriptions. An element of a package lives in the package’s own
namespace, so the move repairs what reached the object from its old place: a module or a yaml of
another subsystem gets import Subsystem::Package or an Import item, the moved files get the
imports of their old subsystem when the move crosses a subsystem boundary, a qualified name - the
full Vendor::Project::Subsystem::FormName.ListRowData of a generated form included - is rewritten,
and a subsystem that starts importing another one gets it in Using. What needs repairing is
decided by the import and visibility rules of the linter, run
before and after the move. Within one subsystem the root and the packages see each other, so the
move adds no import there, and only the full names follow the files. The command refuses a taken
name and a non-public element that another subsystem would reach from now on - the visibility is the
author’s decision. An import the move made unnecessary is named in notes, not removed:
code/unused-import reports it. rename-package (MCP meta_rename_package) renames a package
folder with all its files and rewrites import Subsystem::Batches[::Archive], the Import items
and the qualified names across the project. For a Cyrillic package name in a project with a
translation dictionary both remind of the pair the name needs.
move-resource (MCP meta_move_resource) moves a resource file or a folder of them into another
folder of the same Resources folder; a folder that does not exist yet is created by the move,
because an empty folder is kept neither by git nor by a build. rename-resource-folder (MCP
meta_rename_resource_folder) renames a folder inside it. A resource is addressed by its path under
the folder, so both rewrite the static references that lead to the moved files: the Resource{...}
literals of modules and yaml bindings and the bare values of image properties
(Image: Pictures/Flag.svg). A key with a namespace names the folder of that subsystem or package;
a bare key is looked for in every resources folder of the file’s own subsystem and of the namespaces
it imports, and a key two of them hold is named in notes instead of being rewritten. A lookup by a
computed string (ResourcesPackage.Current().Get("Folder/...")) is resolved at run time: the
commands list such strings by file and line and leave them to the author. delete-resource-folder
(MCP meta_delete_resource_folder) deletes a folder with its files and lists both kinds of
references without editing them; without --apply it prints the plan. A move into the Resources
folder of another subsystem or package is refused: the file would change its namespace, and a lookup
by a string at the old place has nothing to rewrite.
resource-references (MCP meta_resource_references) finds the places that name a resource file or
a folder and changes nothing. It reads the sources the way a move does, and every place comes with its
file, range, line and kind: reference for a static reference, ambiguous for a key held by
several namespaces at the winning priority, string for a string with the path, computed for a string with the folder and a
computed file name, stem for a whole string that spells the key of a file without its
extension. Seed data names a picture by its code this way, and the code adds the extension at run
time; the JSON files of the project’s resources are read for it too. The metadata tree of the VS Code extension shows the answer in the References
view. Resources of the current subsystem take priority over imported namesakes. The root and
packages of that subsystem have equal priority; two matching local namespaces remain ambiguous.
--limit (MCP limit, 100 by default) keeps the first places in the list, and total counts
all of them. When the limit leaves places out, the answer says hasMore: true, and the command
also prints a line on stderr with the --limit that lists them all.
xbsl unused-resources <root> and MCP meta_unused_resources list candidates with no
known uses. They scan resource literals, image properties, resource-loading wrappers,
JSON keys and links from reached CSS/HTML/SVG/JS files. Unreached resource files do not
make their own links reachable. Computed paths and uncertain uses stay separate.
--include-protected includes those separate lists; --limit caps each returned list,
while summary totals remain complete. Every unused entry is labelled confidence: candidate:
external data and arbitrary generated names still need review. The command never deletes files.
Code templates
A template is a short trigger plus a construct. Type the first letters of if, press
Ctrl+Space, pick the template, and the whole statement arrives with edit points to tab through.
The mechanism mirrors the one in 1C:EDT (the code-templates preference page), file format
included.
Templates are offered ahead of the other completions: the construct you are typing out ranks
above a name that merely starts the same. They need no Element data, only the LSP server
(xbsl.lsp.enabled, on by default). The CLI-index mode of the extension does not offer them.
The builtin set is 51 templates, all in xbsl/templates_builtin.py: the control statements, the
declarations (methods with their annotations, structures, enumerations, exception types), queries
and the applied idioms - walking a Catalog, register movements, an HttpService handler,
per-object access permissions, object events, form handlers. Every pattern is parsed by the same
parser the linter runs (tests/test_templates.py), so a template cannot insert code that does
not compile.
A pattern holds edit points and choices. The variables use the ${...} syntax of 1C:EDT
templates. The platform gives them no English spelling, the way it does for the metadata
vocabulary, so the table describes them instead of spelling them out:
| Variable | Expands to |
|---|---|
| edit point | an edit point; its argument is the pre-selected prompt text |
| choice | a dropdown of the fixed variants listed in its arguments |
| metadata name | a dropdown of this project’s objects of the given kind (Catalog, Enum, …), from the index |
| qualified metadata name | the same, inserted as <Kind>.<Name> |
Your own templates live in .xbsl-templates.json at the workspace root; the path comes from
--file or from the xbsl.templates.file setting. The file extends the builtin set, and a
template with the same name replaces the builtin one. Only what differs from the builtin set is
stored, so the next release still reaches you.
xbsl templates list # the whole set: builtin plus your own (* marks yours)
xbsl templates export --output my.json # a dump (to carry your templates to another machine)
xbsl templates import dump.json # merge a dump into your file
In VS Code the same thing is the XBSL: code templates panel, laid out like the EDT dialog: the list with the call context, the description and the pattern, and buttons to add, edit, delete, import, export and restore the defaults. Saving re-reads the set in the running server, so the next Ctrl+Space already offers the edited template.