Commands and API functions
Commands and API functions expose C# business logic to direct API callers — integrations, AI agents, and the product UI. Both are authored in the workspace Net Operation Project with the Moltaro .NET SDK programming model, in the Constructor area under group Automation & logic > Development (page “Business Logic Development”, NET Project mode). Two shapes exist:
- Commands run synchronously: the caller waits and receives the typed result in the HTTP response.
- API functions are global functions enqueued as background jobs: the caller gets a job id and polls its status.
Do not decide between them from an async method or HTTP status. Read
Kind from GET /api/workspace/admin/api-functions (Command or Enqueue)
and the active Function Catalog contract. Moltaro’s enqueue operation returns
HTTP 200 with a queued status model; completion is established only by
polling that model to a terminal state. See
Asynchronous operations and polling.
Synchronous commands
Section titled “Synchronous commands”A command derives from CommandFunction<TArgs, TResult> (or
CommandFunction<TResult> when it takes no arguments). TArgs and TResult
are JSON-serializable types; the runtime deserializes the request Args into
TArgs before the command runs. The command returns one of three outcomes:
CommandResult<TResult>.Ok(result, message)— success with a typed payload;CommandResult<TResult>.Failed(message)— a business failure;CommandResult<TResult>.ValidationFailed(issues)— rejection with field-level validation issues.
Two attributes declare and publish the command:
[MoltaroFunction(id)]assigns the durable function id. The id is also the stable callable key unless an explicitKeyis set; optionalNameandDescriptionare shown to administrators and configurators.[MoltaroCommand]publishes the function on the commands API. Access is fail-closed: exactly one ofPermissionKeyorAllowAnyAuthenticatedUser = truemust be configured, and publications without an access declaration are rejected at build and apply time. Anonymous invocation is never supported. OptionalTimeoutSeconds(1–86400) overrides the synchronous execution timeout; the effective timeout never exceeds the runtime action timeout setting. OptionalLicenseUsageMutationdeclares whether the command can grow, leave unchanged, or only reduce licensed workspace usage.
PermissionKey is a lowercase dotted identifier such as
acme.commands.recalculate-price. It is surfaced in the roles administration
catalog, so administrators grant the command to roles like any other
permission.
using Moltaro.Package.NET.Functions;
public sealed record RecalculatePriceArgs(string ProductCode);
public sealed record RecalculatePriceResult(decimal NewPrice);
[MoltaroFunction("acme-recalculate-price")][MoltaroCommand( PermissionKey = "acme.commands.recalculate-price", LicenseUsageMutation = MoltaroLicenseUsageMutationKindEnum.Neutral)]public sealed class RecalculatePriceCommand : CommandFunction<RecalculatePriceArgs, RecalculatePriceResult>{ public override async Task<CommandResult<RecalculatePriceResult>> OnRunAsync( RecalculatePriceArgs args, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(args.ProductCode)) { return CommandResult<RecalculatePriceResult>.ValidationFailed( new ValidationIssue("Product code is required.", "ProductCode")); }
var newPrice = await RecalculateAsync(args.ProductCode, cancellationToken); return CommandResult<RecalculatePriceResult>.Ok( new RecalculatePriceResult(newPrice), "Price recalculated."); }}LicenseUsageMutation has three values:
Growthmay increase licensed usage. It is the fail-closed default for new, existing, and unspecified publications. Moltaro returns 402 before invoking the command when the latest usage count already reaches a license limit.Neutralcannot change licensed usage. Moltaro does not consult usage for the invocation. Declare it only when all reachable command behavior is usage-neutral.ReducesUsagecan only release licensed usage. It remains callable while a limit is reached.
The runtime resolves one publication for access, mutation classification, and execution. A concurrent publication update therefore cannot admit one version and execute another. Licensed usage is counted in the background, by default every five minutes; the check before a command only reads the latest count. It takes no lock and never waits, so commands and saves of different users never wait for each other because of licensing. Growth committed after the latest count can therefore exceed a limit until the next count.
After an upgrade, existing command publications remain Growth by design.
Changing an existing read-only command to Neutral requires an explicit
LicenseUsageMutation declaration in its Package or Net Operation Project and
a new build/apply cycle so Moltaro can synchronize the authoritative
descriptor. Do not edit the persisted publication row directly.
This descriptor addition came with Package gateway contract 11 and Net Operation Project gateway contract 8; the current SDK uses 15 and 12. The host runs only artifacts of its own contract, so rebuild source projects against the delivered SDK before applying them; see Rebuild after an SDK contract change.
A command that must save several records all-or-nothing as its caller injects
IEntityCompositeMutationService.
Only that call is atomic and replay-safe: invoking the command again runs the
function again, and the repeated composite call with the same OperationKey
returns the stored result.
Invoking a command
Section titled “Invoking a command”POST /api/workspace/commands/{functionKey} with a bearer token (see the
integration quickstart for
authentication). The body is optional; both properties may be omitted:
{ "Args": { "ProductCode": "SKU-100" }, "CorrelationId": "order-4711" }A successful invocation returns the standard envelope with the run result:
{ "Data": { "RunId": "d3f1…", "Message": "Price recalculated.", "Result": { "NewPrice": 129.9 }, "DurationMs": 42, "CorrelationId": "order-4711" }, "Errors": [], "Warnings": [], "Success": true}The error contract:
- 400 with field-level errors when the command returned
ValidationFailed— each issue’s field key appears as the errorField; - 400 with a business error carrying the message from
Failed; - 402 when a
Growthcommand is blocked by a configured on-prem usage limit; - 403 when the caller is authenticated but lacks the permission;
- 404 for unknown, disabled, and retired keys alike — they are indistinguishable, so callers cannot probe which commands exist.
Asynchronous API functions
Section titled “Asynchronous API functions”[MoltaroApiEnqueue] publishes a job-contract
global function for asynchronous
enqueue. The same fail-closed access rule applies: exactly one of
PermissionKey or AllowAnyAuthenticatedUser, never anonymous. Callers with
access can also observe and cancel the resulting queued jobs. Optional
TimeoutSeconds accepts 1–86400 seconds and is captured on each queued job, so
all retries keep the same timeout. When it is omitted, the runtime Job timeout
setting applies. Enqueue always counts as a usage-growth operation because it
persists a new Job; LicenseUsageMutation is a command-only declaration.
[MoltaroFunction("acme-rebuild-products")][MoltaroApiEnqueue( PermissionKey = "acme.jobs.rebuild-products", TimeoutSeconds = 18000)]public sealed class RebuildProductsJob : GlobalFunction{ public override Task<FunctionResult> OnRunAsync( CancellationToken cancellationToken) { return Task.FromResult(FunctionResult.Ok("Product rebuild completed.")); }}POST /api/workspace/functions/{functionKey}/enqueue accepts optional queue
options and returns the queued job status:
{ "Args": { "ProductCode": "SKU-100" }, "DeduplicationKey": "rebuild-SKU-100", "RunAfter": "2026-07-22T18:00:00Z", "CorrelationId": "order-4711"}DeduplicationKey reuses an active queued job instead of enqueueing a
duplicate; RunAfter sets the earliest execution timestamp. Unknown and
disabled function keys answer 404, exactly like commands.
GET /api/workspace/functions/jobs/{jobId}returns the job status — status, timestamps, attempt count, capturedTimeoutSeconds,CanCancel,LastError— plus the latest execution with its result payload and error summary.POST /api/workspace/functions/jobs/{jobId}/cancelcancels a job that is still queued; jobs already claimed by a worker can no longer be cancelled.
A job moves through Queued (waiting for a worker), Leased (claimed by a
worker under an active lease), and ends as Completed, Failed, or
Cancelled. A job is visible to its initiator and to every holder of the
publication permission. Run history and diagnostics for all function
executions live in Operations.
Calling business logic from Workspace UI
Section titled “Calling business logic from Workspace UI”Workspace UI pages use the authenticated MoltaroApiClient instead of
manually supplying bearer or locale headers. It exposes typed command and job
helpers, AbortSignal support, explicit numeric job/run status enums, and a
bounded polling helper:
import { MoltaroApiClient, WorkspaceUiFunctionJobStatusEnum,} from '@moltaro/workspace-ui'
interface RebuildResult { RebuiltCount: number}
const api = new MoltaroApiClient()const queued = await api.enqueueFunction<RebuildResult>('rebuild-report', { args: { Month: '2026-07' }, deduplicationKey: 'rebuild-report-2026-07',})const finished = await api.waitForFunctionJob<RebuildResult>(queued.JobId, { timeoutMs: 120_000,})
if (finished.Status === WorkspaceUiFunctionJobStatusEnum.Completed) { console.log(finished.Run?.ResultData?.RebuiltCount)}The page always runs under the signed-in user’s permissions. A function job
ending as Failed or Cancelled is a successful status read rather than an
HTTP exception; inspect its status, LastError, and latest run. Transport,
authorization, and validation errors keep the standard API error semantics.
See the complete Workspace UI Moltaro API client contract.
Governance
Section titled “Governance”Every command and enqueue publication is listed on the API Functions page
in the Administration area, group Monitoring, with an enable/disable
switch per publication. Disabling a publication makes its key answer 404
immediately, without touching the function source. The same surface is
available as an admin API under /api/workspace/admin/api-functions: list
publications, read one, and update settings with
PUT /api/workspace/admin/api-functions/{publicationId}/settings sending
IsEnabled plus the publication RowVersion for optimistic concurrency.
Publication changes are audit-logged.