Internal API Reference

What It Is

The Internal API is the helper surface available inside PHP-based execution contexts the platform hosts:

  • automation PHP actions
  • canvas PHP items
  • the Cloud Console
  • the Code Inspector
  • code-processor runtime where a PHP handle is exposed

It is exposed as a single object commonly named $api and wraps the PresenterCommons runtime into a stable, builder-friendly shape. Builders should think of it as the "allowed side" of the platform — the parts that are safe to call from user-authored code running inside a tenant.

Why It Matters

The Internal API is how automations, canvases, and advanced diagnostics interact with tenant runtime without reaching into the engine directly.

Using it correctly:

  • keeps scripts portable across environments and versions
  • keeps scripts safe against ACL bypass
  • lets the platform evolve internals without breaking tenant-authored code

Shape Of $api

$api groups its capabilities into themed sub-managers plus a set of flat helpers for the most common actions. The important sub-managers are:

Sub-managerPurpose
$api->systemRuntime/tenant-wide system actions: resource limits, cryptographic rotation.
$api->mediaMedia inspection helpers, such as audio/video playtime.
$api->modelSchema and data-model operations: types, properties, enumerations, implementation snapshots and packages.
$api->createQueryFactory for building Query instances from SQL or from filter structures.
$api->secretsRead and write entries in the tenant Secret Store.
$api->fileRepositoryBasic operations on standalone files in the File Repository.

The top-level $api object also exposes frequently used helpers directly, including local-storage I/O, HTTP fetches, e-mail sending and notification factories, SMS sending, resource lookup, session access, scheduling, flash messages, event logging, Markdown and HTML conversion, and small utilities like randomGenerate. Those are documented inline in the automation and canvas chapters where they are used, except where a capability warrants a dedicated section here.

$api->system

Tenant-wide operational helpers.

getScriptExecutionLimit(): int

Returns the current PHP max_execution_time value (in seconds) for the running context. Useful when a long-running script wants to pace itself against the configured limit.

requestIncreasedMemoryLimit(?int $mb = null): bool

Requests a higher PHP memory limit for the current request. The requested value is capped at 512 MB. If no value is provided, the platform applies a sensible default. Returns whether the request was accepted.

When to use: during a controlled batch operation in an automation or Cloud Console session that processes unusually large data.

Best practice: request only what the operation needs. Increasing the limit is a local hint, not a permanent setting.

regenerateAppSecret(): string

Rotates the tenant's cryptographic security key and returns the new key material. The key protects platform-issued links and short-lived tokens (for example, signed redirect URLs embedded in password-reset e-mails).

When to use:

  • rotating keys on a cadence
  • responding to a suspected key leak
  • scripting tenant-setup flows that need a fresh key

Impact:

  • any link or token already in circulation stops working
  • users or processes holding such links must restart the flow
  • already active logins are not affected

This is the same action exposed in the admin UI under Application Management. Prefer the UI unless you specifically need programmatic access.

$api->media

getPlaytime(string $path): float|int

Returns the playtime of a media file (in seconds) stored under the tenant's local storage. Returns -1 when the platform cannot determine the duration.

When to use: computing derived metadata for uploaded audio or video items.

$api->model

Schema and data-model helpers. These wrap the tenant's type, property, and enumeration managers in a builder-friendly shape that still respects the platform's permission model.

High-level capabilities:

  • createType, importType, updateType, removeType
  • addProperty, updateProperty, removeProperty
  • addEnumerationItems, removeEnumerationItems, renameEnumerationItems
  • getImplementationSnapshot, exportImplementationPackage, previewImplementationPackage, applyImplementationPackage, exportReconcilePackage

These are the programmatic counterparts to what a builder does in the Types, Properties, and Tenant Synchronization screens. Use them when automations need to adjust the model as part of a rollout step, or when a tenant is bootstrapping itself from a package.

$api->createQuery

Factory for Query instances:

  • fromSql(string $sql, array $params = []): Query — a query bound to raw SQL.
  • fromFilters(array $filters, int $typeId): Query — a query built from the standard filter structure used throughout the platform.

Use queries returned by this factory the same way you would consume a configured query from the builder.

$api->query(string $sql, array $parameters = []): SoopioResultSet

Execute a raw SQL statement against the tenant database and return a SoopioResultSet you can iterate over, count, or fetch from. Useful when a script needs an immediate read (or write) the high-level Query factory cannot express — joins, aggregates, CTEs, multi-table subqueries — without leaving the platform's {type.property} shortcode safety net.

Shortcode syntax — the $sql argument is preprocessed before execution; the same shortcode dialect the MCP jetstack_query tool uses applies here, so the SQL stays stable across schema renames:

  • {client} → storage table of the client type
  • {client.name}table_name.column_name for property name
  • {client.name!} → just the column name (no table prefix) — useful in SELECT lists and aliases
  • {project.tags:join} → M:M join table for the tags property on project
  • {project.tags:join.self} → FK column on the join row pointing back to the project side
  • {project.tags:join.other} → FK column on the join row pointing to the tag side
  • {l}, {r} → literal { and } characters

Parameters. Use ? placeholders and pass values in $parameters (positional). Never interpolate user input into the SQL string.

SELECT discipline. Every referenced table should be filtered with is_deleted = 0 — the platform's soft-delete contract is enforced through that column, and raw SQL bypasses the higher-level filters that add it automatically.

Mutations. INSERT, UPDATE, DELETE work, but writes through this surface bypass the platform's row-level permission filters. For typed-object writes prefer the $api->model actions or $api->getResource(...)-based update flows — they go through PermissionsManager and the standard write pipeline. Use $api->query() mutations only for cases that legitimately need raw SQL (bulk schema-aware updates, denormalized auxiliary tables, etc.).

Datetime parameters. MySQL session timezone is pinned to UTC, so the parameter value is interpreted against that. ISO 8601 strings with explicit zone info (Z, ±HH:MM) are honored and converted to UTC by MySQL itself; naive strings ('2026-05-22 09:00:00') are compared as UTC. When the parameter is a user-supplied wall-clock value, wrap it in $api->toUtc(...) first so the conversion uses the user's display tz instead of UTC.

Return value. A SoopioResultSet. Common operations:

  • count(): int
  • fetchRow(?string $property = null): mixed
  • fetchObject(bool $cache = true): ?object
  • fetchAllRows(?string $path = null): array
  • fetchObjects(...)

When the leading FROM clause references a type's storage table, the result set knows the type id and can hydrate rows as proxy resources. For free-form result shapes (no recognizable FROM <type> or aggregate-only output) the result set returns raw rows.

When to use: reporting queries, dashboards backed by aggregates, cross-type joins, and any read pattern that the platform's filter/order/page UI does not cover. For single-row reads against a known type, prefer $api->getResource(...). For typed list queries with filters, prefer $api->createQuery.

Example — aggregate report:

$result = $api->query(
    'SELECT {project.client!} AS client_id, COUNT(*) AS open_count
     FROM {project}
     WHERE {project}.is_deleted = 0 AND {project.status!} = ?
     GROUP BY {project.client!}',
    ['open']
);
foreach ($result->fetchAllRows() as $row) {
    // $row->client_id, $row->open_count
}

Example — filter with user-supplied cutoff:

$cutoff = $api->toUtc($userInput); // converts wall-clock to UTC DateTimeImmutable
$result = $api->query(
    'SELECT * FROM {invoice} WHERE {invoice}.is_deleted = 0 AND {invoice.due_on!} < ?',
    [$cutoff]
);

$api->toUtc(mixed $input, ?string $sourceTz = null): ?\DateTimeImmutable

Convert a wall-clock value to a UTC DateTimeImmutable suitable for query parameters, storage, or direct comparison against DATETIME columns.

The platform stores every DATETIME value in UTC, but most user-facing inputs are wall-clock values in a display timezone. Constructing a raw new DateTime($input) interprets naive strings as UTC and silently shifts them by the user's display-tz offset. toUtc() does the right thing for every accepted shape:

InputBehavior
ISO 8601 with explicit zone (Z, ±HH:MM)zone honored, value shifted to UTC
Naive string (2026-05-22 09:00, tomorrow, +1 day)interpreted in $sourceTz if given, otherwise in the user's display tz, then shifted to UTC
DateTimeInterfacecoerced to immutable, shifted to UTC
int or numeric stringtreated as Unix timestamp
null or empty stringreturns null

Returning a DateTimeImmutable rather than a formatted string lets callers do further math; Nette's database layer formats it correctly when passed as a query parameter because it carries UTC.

Use this instead of new DateTime($userInput) whenever the input might be a user-supplied wall-clock. The matching CodeProcessor expression is toUtc(...) — note that in expressions it returns a formatted string (Y-m-d H:i:s, SQL-safe) rather than a DateTimeImmutable, because expressions feed query() parameters and JSON payloads where a flat scalar is what callers need. For the reverse direction (UTC value back to display), see $api->fromUtc(...).

For full timezone-handling rules — including which integration paths normalize automatically and which require this helper — see REST API → Timezones.

$api->fromUtc(mixed $input, ?string $targetTz = null): ?\DateTimeImmutable

Reverse of toUtc(). Converts a value assumed to be UTC into a DateTimeImmutable shifted into the user's display timezone (or $targetTz if given), ready to render in operator-facing surfaces.

The platform stores every DATETIME value in UTC, but most user-facing surfaces want times shown in the operator's local wall-clock. Constructing a raw new DateTime($stored) keeps the value as UTC and silently mislabels it once it lands in a UI or an e-mail. fromUtc() does the right thing for every accepted shape:

InputBehavior
ISO 8601 with explicit zone (Z, ±HH:MM)zone honored as the source, value shifted to the target tz
Naive string (2026-05-22 09:00)interpreted as UTC (that is the inverse of toUtc()'s contract), then shifted to the target tz
DateTimeInterfacecoerced to immutable, shifted to the target tz
int or numeric stringtreated as Unix timestamp
null or empty stringreturns null

Returning a DateTimeImmutable lets the caller keep doing math: compare against another local datetime, format with a custom pattern, embed in a notification payload.

Use this whenever a script reads a UTC datetime from the database and needs to expose it to a user, embed it in a message, or compare it against a wall-clock condition. The matching CodeProcessor expression is fromUtc(...) — note that in expressions it returns a formatted string (ISO 8601 with offset by default) rather than a DateTimeImmutable, because expressions feed JSON and templates where a flat scalar is what callers need.

For full timezone-handling rules, see REST API → Timezones.

$api->secrets

Wraps the tenant Secret Store. Use it to read credentials an automation or canvas needs — never hard-code secrets in scripts.

$api->fileRepository

Basic operations on standalone files in the tenant File Repository. Files attached as properties on typed objects are managed implicitly through the owning object's normal save path and are not exposed here.

list(array $filters = [], int $limit = 500, int $offset = 0, string $sort = 'file_name', string $dir = 'asc'): array

Returns rows for standalone files in the repository index. Accepts the same filter keys as the repository UI (search, folder_id). Each row includes the file's repository uuid, file_name, size, modified, folder_id, is_locked, and owner_id.

When to use: enumerating files for reporting, auditing, or batch processing.

get(string $uuid): ?array

Returns a single standalone file row by its repository uuid (hex). Returns null when the uuid is unknown or has been cleared.

getUrl(string $uuid): ?string

Resolves the current storage URL for a standalone file. Returns null when the file is gone, not yet materialized (async pending), or the caller has no permission.

When to use: embedding a file link inside a generated e-mail, notification, or exported payload at the moment of use.

upload(string $localPath, ?string $fileName = null, ?int $folderId = null): int

Creates a new standalone file from a local path. The file is stored as a private MediaFile, registered in the repository index, and optionally placed into the given repository folder. Returns the new MediaFile id.

When to use: persisting a file an automation has just produced (a rendered PDF, an exported CSV, a generated image) so that operators can find and govern it in the repository.

rename(int $mediaFileId, string $fileName): void

Renames a standalone file. Locked files cannot be renamed.

move(int $mediaFileId, ?int $folderId): void

Moves a standalone file into a different repository folder. Pass null to move it back to the root. Locked files cannot be moved.

delete(int $mediaFileId): void

Soft-deletes a standalone file and removes it from the repository index. Locked files cannot be deleted.

rebuildIndex(): int

Rebuilds the entire repository index from source data. Returns the resulting row count. Run after bulk imports, schema migrations, or data restores — normal operations stay in sync automatically.

getTotalSize(array $filters = []): int

Returns the total byte size of indexed files matching the filters. Useful for custom storage-reporting dashboards.

Variables And Parameters

Top-level helpers for reading and writing the variables that surround a running script. These mirror the Code Processor builtins (var(), templateVar(), env(), appParameter(), selection()) — $api is the read-and-write counterpart that PHP-mode actions, canvas items, and the Cloud Console reach for when they want to participate in those same scopes.

All four scopes (var, templateVar, env, appParameter) resolve through the same CodeProcessorProvider::resolve* helpers the expression engine uses, so the two surfaces never drift on semantics.

getVariable(string $name): mixed

Reads a local variable from the current execution context (the running Automation or Canvas). Returns null when no context is bound or the variable is not set. Equivalent to var('name') in expressions.

setVariable(string $name, mixed $value): void

Writes a local variable on the current execution context. No-op when no context is bound. Use this to pass values across automation actions or canvas items within one run.

getTemplateVariable(string $name): mixed

Reads a variable from the current focus template (the Latte template the request is rendering). Returns null when there is no focus template or the variable is not set. Equivalent to templateVar('name') in expressions.

setTemplateVariable(string $name, mixed $value): void

Writes a variable onto the current focus template. Throws InvalidStateException when no focus template is available, and InvalidArgumentException when $name is not a valid PHP identifier. Use this when an automation or canvas item needs to surface a value to the surrounding rendered page.

getEnvVariable(string $name): mixed

Reads an environment variable from app settings. Returns null for unknown names — reserved Config keys never leak through this helper, even if the underlying settings store happens to expose them. Equivalent to env('name') in expressions.

Why no setter: environment variables are admin-configurable through the Customization UI, not script-settable. Use $api->secrets for runtime-rotatable values.

getAppParameter(string $name): mixed

Reads an app/request parameter (the same shape appParameter('name') returns in expressions). When $name is presenterName the value comes from the running presenter; otherwise from the current request's parameter bag.

getSelection(): mixed

Shortcut for the selection app parameter. Returns a unique, filtered array combining the request's id parameter and the selection parameter (a CSV of ids) — the standard shape view actions and bulk operations consume.

Sending E-mail

The top-level $api exposes two ways to produce e-mail from inside a script:

  • $api->sendEmail(...) — a one-shot helper for the common case: "send this message to these recipients, now."
  • $api->createEmail(): Email — a builder-style object when a script needs finer control (multiple recipients configured in steps, conditional content assembly, manual send() timing).

Most automations and canvases should reach for sendEmail(). Use createEmail() only when the shape of the message is computed across several code paths.

sendEmail(bool $async, string|array $to, ?string $subject = null, ?string $body = null, int|string|null $template = null, array $vars = [], File|string|array|null $attachments = null, ?string $from = null, ?string $replyTo = null): void

Call this helper with named arguments — the signature groups delivery and content parameters in a fixed order that is not meant to be used positionally. Both async and to are required; the caller must pick a delivery mode explicitly every time.

Sends a single e-mail to one or more recipients. The caller provides either a stored E-mail Template (by id) or a raw body — never both. If neither is given, the call fails.

Recipient format. to, from, and replyTo accept the standard "Display Name" <address@example.com> convention. to also accepts a comma-separated string ("a@x.com, b@y.com") or a PHP array of such entries.

Template vs. body.

  • When template is provided, the platform resolves the template resource, validates that it is active, and renders it. subject, vars, and attachments still apply; body is ignored.
  • When only body is provided, the platform wraps it in the tenant's default e-mail layout (the same one the Send an e-mail automation action uses when no template is selected) and renders it with vars.

Attachments. Pass a single entry or an array. Each entry can be:

  • a File Repository file (or any object implementing the File interface) — the platform reads its content and filename automatically.
  • a local path string — attached as-is.

Sync vs. async delivery. async = false hands the message to the configured mailer right away — use it for low-volume, user-facing flows where the caller needs to know the send happened before returning. async = true queues the message for the platform's bulk-send cron and returns immediately. Prefer async = true inside long-running automations or batch loops so that a single outbound failure does not abort the run.

When to use:

  • Transactional notifications from an automation (order confirmation, password reset, approval outcome)
  • Operator alerts triggered by a scheduled check
  • Custom messages composed from a canvas step

Best practices:

  • Prefer e-mail templates over inline bodies for anything a non-developer may need to edit later.
  • Resolve dynamic content through vars rather than by string-concatenating into body — it keeps the template portable and the rendering cacheable.
  • For blast-style sends inside a loop, pass async: true and let the queue smooth the outbound rate.

Example — transactional, template-based:

$api->sendEmail(
    async: false,
    to: $user->email,
    subject: 'Your order is confirmed',
    template: $orderConfirmationTemplateId,
    vars: ['order' => $order, 'user' => $user],
);

Example — inline body with attachment, queued for cron:

$api->sendEmail(
    async: true,
    to: '"Ops" <ops@example.com>',
    subject: 'Nightly export ready',
    body: '<p>The export is attached.</p>',
    attachments: $exportFile,
);

Logging Events

Scripts can append entries to the platform Event Log — the tenant-wide, append-only ledger that operators use to answer "what happened, when, and on whose behalf?". Events written from a script appear alongside platform-emitted events (account lifecycle, object CRUD, e-mail/SMS/push deliveries) and are visible in the Event Log admin screen.

logEvent(string $title, ?string $category = null, ?string $content = null, mixed $data = [], ApiResourceProxy|ActiveResource|array|null $object = null, int|string|null $objectType = null, int|string|null $objectId = null, int|string|null $userId = null): Event

Records a single entry. Designed to be cheap to call: only title is required — every other field has a sensible default pulled from the current execution environment, and any default can be overridden.

Defaults pulled from the environment:

  • categorygeneral (the General Activity category) when not provided.
  • userId → the current signed-in user's id.
  • objectType / objectId → derived from $object when an object is provided.
  • owner and created_by columns are filled automatically by the underlying ledger — the actor who initiated the request, plus the shadow user when an admin is impersonating.

Pinning to an object. Pass $object as one of:

  • a resource handle (the proxy returned by $api->getResource(...) or by a query loop),
  • a raw [typeId, id] array,
  • or omit $object and pass objectType / objectId directly.

When both $object and explicit objectType / objectId are given, the explicit values win — useful when the script wants to attribute the entry to a different object than the one currently in scope.

Category. The platform ships a small set of canonical categories (account lifecycle, object CRUD, messaging deliveries, cron, general). Scripts should reuse the existing categories where they fit — operators filter and dashboard against them. Use the default general category for ad-hoc business events that do not match a built-in one.

data. Free-form structured payload (array or scalar). Stored as JSON on the entry and rendered in the detail modal. Keep it shallow and operator-readable: the goal is to give a human enough context to understand what happened, not to reconstruct full state.

When to use:

  • recording a domain-meaningful business event (e.g. Order shipped, Refund approved, Bulk import finished) so it shows up next to platform events in the same operator-facing surface
  • annotating a one-off administrative action a script took on a record
  • emitting structured progress or outcome markers from a long-running automation that operators may need to audit later

When not to use:

  • as a debug log — the Event Log is for events of operational interest, not verbose tracing.
  • for high-volume per-row tracing inside a tight loop — emit a single summary entry at the end instead.
  • for storing secrets, tokens, or PII the operator is not allowed to see — Event Log entries are visible to anyone with the View Event Log permission.

Best practices:

  • Treat title as the headline a busy operator will scan; put the noun and the verb in there.
  • Put narrative detail in content; put machine-readable detail in data.
  • Pin to the object the event is about, not the object that happened to trigger the script.

Example — minimal, current-user-attributed business event:

$api->logEvent(title: 'Order shipped');

Example — pinned to the object in scope:

$api->logEvent(
    title: 'Order shipped',
    content: 'Carrier: DHL, tracking 1Z999AA10123456784',
    data: ['carrier' => 'DHL', 'tracking' => '1Z999AA10123456784'],
    object: $order,
);

Example — system-attributed batch summary at the end of a long automation:

$api->logEvent(
    title: 'Nightly reconciliation finished',
    category: 'general',
    content: "Processed {$count} rows, {$failed} failed.",
    data: ['processed' => $count, 'failed' => $failed],
    userId: null,
);

Markdown And HTML Conversion

Scripts often need to move content between Markdown and HTML — rendering user-authored Markdown for display, or ingesting rich HTML from an external system and storing the canonical Markdown form. The top-level $api exposes a pair of helpers for these round-trips, backed by league/commonmark and league/html-to-markdown.

markdownToHtml(string $markdown, string $flavor = 'gfm'): string

Renders a Markdown source to HTML.

Flavor. Defaults to gfm — the GitHub Flavored Markdown superset, which adds tables, strikethrough, task lists, and autolinks on top of CommonMark. Pass commonmark for strict CommonMark when the downstream consumer expects exactly that.

When to use:

  • rendering operator-authored Markdown notes into an HTML surface (e-mail body, canvas item, notification content)
  • normalizing Markdown pulled from an external source before embedding it

Example:

$html = $api->markdownToHtml($ticket->description_md);

htmlToMarkdown(string $html, array $options = []): string

Converts an HTML fragment to Markdown.

Options. Forwarded to HtmlConverter and override its defaults. The commonly useful keys are:

  • header_styleatx (# Header) or setext (underline). Default: setext.
  • strip_tags — strip HTML tags without Markdown equivalents (useful for cleaning MS Word output). Default: false.
  • hard_break — render <br> as \n instead of \n. Default: false.
  • list_item_style-, *, or + for <ul> bullets. Default: -.
  • remove_nodes — space-separated list of DOM nodes to drop (e.g. 'meta style script').

When to use:

  • capturing rich content from a WYSIWYG editor or an imported e-mail and storing it as Markdown
  • normalizing HTML pulled from a third-party API into the tenant's canonical Markdown form

Example:

$markdown = $api->htmlToMarkdown($emailHtml, [
    'header_style' => 'atx',
    'strip_tags' => true,
]);

Contract Expectations

  • Internal API calls respect tenant ACL unless the calling context explicitly lifts it.
  • Internal API shape is additive across versions: new sub-managers and methods may appear; existing ones aim to remain stable.
  • Destructive or cryptographic actions (for example regenerateAppSecret) are intentional, tenant-wide events. Script them deliberately.