core/* Module Reference

Every Tengo module the engine exposes, with signatures and gotchas.

Loading order

The Tengo adapter (internal/coreapi/tengo_adapter.go) registers built-in modules at engine boot. They are always available with import("core/<name>"); capability checks fire at call time, not import time, so importing a module you don't have permissions for is harmless. Module objects are immutable from script perspective — you can't monkey-patch core/log or replace core/nodes.create.

Every module is also exposed through the gRPC SquillaHost service to compiled Go plugins (under different naming — QueryNodes instead of core/nodes.query, SendEmail instead of core/email.send). The capability set is the same; only the surface differs.

core/log

Structured leveled logging that lands in the kernel's structured log stream (stdout in dev, the configured log sink in production). Caller info (theme/extension slug, script path) is auto-prefixed onto every line.

  • log.info(message, fields?) — informational; the most common level.
  • log.warn(message, fields?) — something is off but not broken.
  • log.debug(message, fields?) — hidden behind SQUILLA_LOG_LEVEL=debug; use freely without rate-limiting.
  • log.err(message, fields?) — alias because error is a reserved Tengo identifier.

fields is a Tengo map of structured key-value pairs. Avoid embedding values into the message string — the structured fields make grepping and aggregation tractable.

core/nodes

  • nodes.get(id) → Node object, or error.
  • nodes.query({node_type?, status?, language_code?, slug?, search?, limit?, offset?, order_by?, parent_id?, taxonomies?}){nodes: [...], total: N}. Default limit 25, max 200. order_by takes the same vocabulary as the SQL ORDER BY clause (created_at DESC, title ASC); the kernel validates against an allow-list to prevent injection.
  • nodes.create({title, node_type, status, slug?, layout_slug?, language_code?, fields_data?, blocks_data?, seo_settings?, featured_image?, taxonomies?, excerpt?, parent_id?}) → Node. Auto-generates the slug from the title if omitted. Fires node.created event asynchronously after commit.
  • nodes.update(id, partial_input) → Node. Same shape as create; pass only the fields you want to change. Fires node.updated.
  • nodes.delete(id) → undefined. Soft-delete: the row is hidden but preserved (revisions remain). Hard-delete is admin-only and not exposed to scripts.

core/nodetypes

  • nodetypes.register({slug, label, label_plural, icon?, description?, taxonomies?, field_schema?, url_prefixes?}) → NodeType. Idempotent on slug; calling twice with the same slug updates the existing definition.
  • nodetypes.get(slug) → NodeType.
  • nodetypes.list() → [NodeType].
key vs name
nodetypes.register field_schema entries use "name":. block.json field_schema entries use "key":. Mixing them produces empty admin inputs even when fields_data is correct. The asymmetry is historical; the loaders refuse to translate one to the other on your behalf because the silent translation would mask far worse bugs.

core/taxonomies

  • taxonomies.register({slug, label, label_plural, hierarchical?, show_ui?, node_types?, field_schema?}) — register a taxonomy attached to one or more node types. hierarchical: true means terms can have parent_id (categories); false is flat (tags).
  • taxonomies.get(slug) / taxonomies.list() / taxonomies.update(slug, partial) / taxonomies.delete(slug)

core/terms

  • terms.list(node_type, taxonomy) → [Term].
  • terms.get(id) → Term.
  • terms.create({node_type, taxonomy, slug, name, description?, parent_id?, fields_data?}) → Term. Note the {slug, name} object form when storing terms on nodes — see core.guide's term_field_shape gotcha for why bare slugs are rejected by the admin.
  • terms.update(id, updates_map) → Term.
  • terms.delete(id) → undefined. Cascades to remove the term from every node that referenced it.

core/menus

  • menus.upsert({slug, name, items: [{label, url|page, target?, children?}]}) → Menu. Idempotent on slug. The page: "<node-slug>" form resolves to the node's id at upsert time, so renaming the page later doesn't break the menu — the menu item points at the node by id, and the URL is regenerated from the node's current slug on every render.
  • menus.get(slug) / menus.list() / menus.delete(slug)

core/settings

  • settings.get(key) → string. Returns "" if missing; check with == "" rather than is_undefined.
  • settings.set(key, value) — emits setting.updated event with {key, language_code, value} after commit. Sensitive-shaped keys (matched by suffix patterns like :secret, :token, :password) are auto-encrypted at rest using the AES-256 master key.
  • settings.all(prefix?){key: value, ...}. Useful for fetching a whole namespace (settings.all("squilla.brand.")).

core/theme_settings

  • theme_settings.active_theme() → slug, or "" if no theme is active.
  • theme_settings.pages()[{slug, name, file, icon}] for the active theme.
  • theme_settings.get(page_slug, field_key) — requires theme_settings:read capability. Theme scripts get this automatically; extensions must declare it.
  • theme_settings.all(page_slug) → field map keyed by field key.

core/events

  • events.emit(action, payload?) — fire-and-forget, non-blocking. Goroutine-dispatched; the call returns immediately.
  • events.on(action, script_path, priority=50) — subscribe.
  • events.subscribe(action, script_path, priority=10) — same as on, just a lower default priority. Use whichever reads more naturally; the priority can be overridden anyway.

core/filters

  • filters.add(name, script_path, priority=10) — register a filter handler. Lower priorities run first.

core/routes

  • routes.register(method, path, script_path) — mount an HTTP handler. Paths containing a dot mount at the top level (/sitemap.xml); other paths mount under /api/theme/ for theme scripts. Extensions should prefer the manifest's public_routes for stable URLs.

core/wellknown

  • wellknown.register(path, script_path) — handler under /.well-known/<path>. path is the suffix ("security.txt", "acme-challenge/*"). Trailing * is a prefix wildcard; everything after the wildcard lands in request.params.rest.

core/http

  • http.get(url, options?) / http.post(...) / http.put(...) / http.delete(...) — method-specific shortcuts.
  • http.fetch(method, url, options?) — generic.

Options: {headers: {...}, body: "...", timeout: 30} (timeout in seconds, capped by the 10-second script wall-clock so anything larger is a soft hint, not a hard ceiling). Returns {status_code, body, headers, error?}. Outbound requests honor a configurable allow-list and block private network ranges by default to prevent SSRF.

core/email

  • email.send({to, subject, body, html?, from?, reply_to?, cc?, bcc?, headers?, template_id?, template_data?}) — enqueue an email through the dispatcher. Routes through the email.send event; an extension declaring email.provider must be active or the call fails with "no provider". When a template id is set, the email-manager extension renders the template against template_data before handing off to the provider.

core/media

Available only to extensions that declare the relevant media:* capability — themes do not get media access by default.

  • media.upload({filename, mime_type, body_base64, alt?, caption?, tags?}) → MediaFile ({id, slug, url, ...}). Routes through whichever active extension declares media-provider.
  • media.get(id) / media.query(query) / media.delete(id)
  • media.import_url(url, opts?) — fetch a remote URL and import as a media file. Honors the SSRF allow-list.

core/data

Raw row CRUD against extension-owned tables. Subject to the data_owned_tables ownership check on top of the capability check; kernel-private tables (users, sessions, content_nodes, etc.) are denied unconditionally. Themes do not get this module.

  • data.get(table, id) → row map.
  • data.query(table, {where?, order_by?, limit?, offset?}){rows, total}.
  • data.create(table, data_map) → row map.
  • data.update(table, id, data_map) → undefined.
  • data.delete(table, id) → undefined.

core/files

  • files.store(path, data_bytes_or_string) → string (final path).
  • files.delete(path) → undefined.

Paths are relative to the configured storage root; absolute paths and traversal escapes are rejected. Used for assets that don't belong in the media library (cached renders, generated PDFs, etc.).

core/assets

  • assets.read(rel_path) → string. Path is resolved against the theme/extension root (where the script lives); absolute paths and traversal escapes are rejected at resolution time.
  • assets.exists(rel_path) → bool.

core/users

  • users.get(id) → User.
  • users.query({role?, search?, limit?, offset?}){users, total}.

Read-only. Scripts cannot create, update, or delete users — those operations live behind admin-authed handlers in the kernel.

core/helpers

  • helpers.json_encode(value) → string. Returns "null" on encode error.
  • helpers.json_decode(string) → any. Returns undefined on parse failure; check with is_undefined.

core/routing

Available only inside render-scope scripts (filter or event handlers running during a public-page render). Outside the render scope these calls return zero values.

  • routing.is_404() → bool.
  • routing.is_homepage() → bool.
  • routing.site_setting(key) → string. Convenience for reading a site setting from inside a render-context handler without re-importing core/settings.

Standard library

The Tengo standard library is enabled: fmt, json, math, times, rand, enum, text, plus a stripped-down os with only os.read_file_lines redirected through the kernel's asset resolver. See the upstream Tengo docs at github.com/d5/tengo for full signatures and quirks.