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.
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")]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."); }}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; - 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.
[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.