Manage metadata with the hf CLI
Run the full agent-authoring loop — log in to a target, write metadata to
a cart branch, review the DDL and the changed files, publish, and query
the result — from the hf CLI, without a browser or Studio. This is the
headless counterpart of
Plan and publish a cart branch,
which shows the same routes over raw curl.
Prerequisites
Section titled “Prerequisites”- A running Henceforth server and its URL.
- A principal holding
admin.metadata.plan(forhf branch plan),admin.metadata.apply(forhf applyandhf branch publish), andadmin.metadata.module.read(forhf branch diff).hf branch planalso needs globaladmin.metadata.object.readandadmin.metadata.field.read: the computed diff names every module, object and field it touches, soadmin.metadata.planalone is not enough — a principal with the plan grant but no read grants gets403. All of these are scoped globally, not per-module. Writing directly tomain(hf module|object|field create|update|delete --branch main) requiresadmin.metadata.applytoo, in addition to the operation’s own permission — a create/update/delete-only principal gets403on--branch main. Every step below writes to a cart branch instead, which needs no apply grant at all; this walkthrough works the same whether or not your principal holdsadmin.metadata.apply, until the plan and publish steps. - The
hfbinary on yourPATH.hf --versionconfirms it is installed.
Add --json to any command below to print the server’s response body
verbatim instead of the human-readable rendering shown here.
1. Log in
Section titled “1. Log in”hf login my-server https://your-server --sshDrop --ssh to sign in with a username and password instead. On an
instance nobody has claimed yet, --ssh claims it: hf asks for an
administrator name, confirms, and signs in with the key the instance was
started with (see the authentication architecture for why).
This stores a target named my-server with the token you authenticate
with, and makes it the current target if none is set yet. Every later
command in this walkthrough runs against the current target; pass
-t my-server (or any target name) to override it, or -t a different
target to point the same commands at a different environment.
2. See what is there
Section titled “2. See what is there”hf module listhf object list corehf field list core.AccountEach listing prints the dotted key path of every entry with its label; the
path is what the other verbs take as their subject. hf object list and
hf field list with no argument list across every module. hf object show core.Account prints one record.
3. Write to a cart branch
Section titled “3. Write to a cart branch”create, update and delete all require --branch explicitly — there
is no implicit main, so writing straight to production is a deliberate
--branch main, never the default:
hf module create --branch feat-x sales -f sales-body.json5hf object create --branch feat-x sales.Lead -f lead-body.json5create maps to the server’s POST (fails with 409 METADATA_RESOURCE_EXISTS if the resource already exists); update maps
to PUT (fails with 404 if it does not). The file is the request
body only — the resource’s own fields (label, description, and so
on) — not the resource’s key: moduleKey/objectKey come from the
sales/sales.Lead argument on the command line, and the write routes
reject a body that also includes them. A field body is the exception: it is
a whole field definition, and its fieldKey must match the path. See the
HTTP API reference for each resource’s exact request body
shape. As many writes as needed land on the same branch before review —
each is its own commit on feat-x.
4. Plan the branch
Section titled “4. Plan the branch”hf branch plan feat-xLists the changes publishing feat-x would make: each change’s target
and risk label, with its DDL statements indented underneath (every line
of a multi-line statement carries the same indent), an aggregate summary,
and, first, a line naming the branch and the exact commit the plan was
computed against. Nothing has run yet — hf branch plan exits 0 whether or
not the plan is empty, because a non-empty plan is not itself an error.
Plan never hides a change. Every change carries a disposition —
execute, skip, or refuse — computed server-side from the same
predicate the publish/apply gate itself uses, so a client reads it
directly instead of deriving “would this be refused?” from risk or
any other field. hf branch plan renders it as a label before the
risk label: [SKIPPED] for skip, [REFUSED] for refuse, nothing
for execute. An orphan drop (a column, junction table, or module
schema no longer in metadata) is skip under this run’s options —
still listed, with its DDL and risk; the summary line’s trailing
skipped: N counts them. Publishing without --destructive leaves a
skip change in place (with a warning), rather than running it; add
--destructive to hf branch plan to see what it would do.
The managed Lookup-to-Junction FK column drop a relationship-strategy
migration itself already decided is safe is always execute, even
without --destructive — never skip. A change rated destructive
risk that apply cannot skip is what makes disposition: refuse
appear: an unsafe USING type narrowing (for example an integer
field going from big to regular) or a generated column’s storage
mode changing (a drop-and-re-add). Publishing refuses the whole run
without --destructive when any change in the plan carries
disposition: refuse, naming each such change in the error, rather
than leaving just that one change in place.
5. Review what files changed
Section titled “5. Review what files changed”hf branch diff feat-xLists the files the branch adds, modifies, or deletes relative to
main, independent of what DDL those changes imply. This only needs
admin.metadata.module.read — a reviewer who can read metadata but not
apply it can still see what changed.
6. Publish
Section titled “6. Publish”hf branch publish feat-xOn a terminal, this confirms before publishing (skip the prompt with
--yes, required when running non-interactively or with --json — the
same non-interactive guard hf apply --destructive uses, so a script or
an agent can never hang on a prompt it cannot answer). Publishing
fast-forwards main onto the branch and applies it in one step; the
response is the same shape hf plan/hf apply render, with status: applied and the branch and commit that were published named in the
source line. On success the branch is deleted and main’s HEAD has
advanced.
7. Query the result
Section titled “7. Query the result”hf query "SELECT id, name FROM core.Account LIMIT 10"hf query parses the HOQS text locally before any network request: a
syntax error renders with the parser’s own caret diagnostics and never
reaches the server. A valid query prints a column-aligned table (or, for
a query read from a file, hf query -f query.hoqs) with a trailing N of M line and a hint when more rows are available.
Verification
Section titled “Verification”hf plan (planning the server’s own working tree) now reports no changes
for whatever feat-x added — its change list is empty, because main
already has what the branch had. hf object show sales.Lead reads the
published object.
- Publishing an unknown branch answers
404 BRANCH_NOT_FOUND; it never creates the branch as a side effect of the lookup. - If a branch’s metadata fails to compile, publish answers
422naming every diagnostic and leavesmainunmoved. - A branch id containing
/(feat/x) is percent-encoded automatically by every verb that takes--branchor a positional branch argument — the server’s write routes require it as a single path segment. - A module key may be vendor-qualified:
hf object show acme.crm.Accountreads theAccountobject of theacme.crmmodule. The object key is the first segment that starts with an uppercase letter. Vendor modules are read-only, so the writes take a bare module key. --jsonprints the server’s success body verbatim with exit0, or its{"errors": [...]}envelope verbatim on stderr with exit1on failure — no wrapping, no re-serialization, so a script can pipe the output straight intojq.
See also
Section titled “See also”- The command references:
hf module,hf object,hf fieldandhf branch. - Plan and publish a cart branch — the same flow over raw HTTP, with every route’s exact request and response shown.
- From metadata to database — how a metadata write becomes a database schema.
- HTTP API reference — the rendered OpenAPI document
every
hfverb in this guide is a thin client over.