.NET SDK for C# business logic
The Moltaro .NET SDK is the programming model for C# business logic in the workspace Net Operation Project: a function is a C# class that derives from an SDK base class and declares identity, bindings, and publication with attributes. Authoring happens in the Constructor area under Automation & logic > Development, or entirely through the Configuration API. AI agents should use the API-first authoring flow; downloading the NET Project ZIP is optional for a human using a local IDE.
Use the curated .NET reference together
with the installation’s GET .../net-operation-project/developer-surface
catalog. The catalog, not arbitrary public CLR visibility, defines supported
assemblies and injectable services.
For durable publication, related Board/Workflow facts and the versioned
ContextChanged contract, see Resource events.
For Workflow, use its configuration automation and runtime services. Runtime commands support Action, Command, HttpEndpoint, Job and TriggerHandler; configuration automation supports the first three. The process document reference describes the Engine types, version-2 YAML and durable typed observations.
Community Memberships exposes community commands and reads through
ICommunityCommandService and ICommunityQueryService. Its Workspace payment-method
catalog provides IPaymentMethodQueryService for list/point reads and
IPaymentMethodCommandService.CreateAsync for creating enabled methods. The latter
takes CreatePaymentMethodRequest with a display name, optional description and a
stable OperationId; an unchanged retry returns the original ID and RowVersion.
Both command services return MembershipCommandResult with ObjectId,
DisplayName and RowVersion. The optional request CorrelationId links
diagnostics; OperationId identifies an operation for exact retries.
These services share one scoped IActorContextProvider. An empty actor means
trusted code and writes system attribution; set ActorUserId to a real user ID
to check that user’s module permissions. Payment-method reads require Workspace
payment-method.read; creation requires Workspace payment-method.manage and an
enabled module. A new command transaction uses ReadCommitted; an existing caller
transaction retains its isolation through a savepoint. Creation writes typed
history and a command receipt atomically. The catalog classifies recorded payments;
it configures no payment provider. Membership issuance, corporate groups and
payment posting are not yet supported commands, and administrative module status
continues to report RuntimeAvailable=false.
Formatting dates and times
Section titled “Formatting dates and times”Moltaro.Package.NET.Formatting.MoltaroDateTimeFormatter provides common server-side
display formatting for DateOnly, TimeOnly, DateTime and DateTimeOffset. Its
constructor takes a CultureInfo, a TimeZoneInfo and the resolved .NET date, time
and date-time format strings. Use the workspace formats/time zone and choose the
language for the output’s audience; stored shared text uses workspace language.
Formats use .NET tokens with literal / and : separators, keeping the workspace
pattern’s punctuation when language changes. The formatter normalizes separators
in its own read-only culture copy and leaves the supplied culture unchanged.
Call the typed Format(value) overload. Calendar dates and wall-clock times are
not shifted. Instants are converted to the configured zone. A DateTime with
Kind.Unspecified is wall time in that zone; a DST gap or overlap throws
ArgumentException, so supply a UTC DateTime or DateTimeOffset when the instant
matters. Kind.Local follows .NET’s source-machine local-zone semantics. The formatter
does not parse strings, read ambient culture, or append a zone label by itself.
Existing presentation placeholders use this formatting automatically. No formatting function needs to be added to a template.
Command failure details
Section titled “Command failure details”For governed Entity commands, SecurityMutationError.Metadata preserves named
decision details from the server. The mutation receipt journal has no
receipt-count limit. See
reliable automation
for the seven-day replay window and recovery rules. ThrowIfFailed() retains
these details in MoltaroSdkException. Aggregate SafeFailure items carry only
the stable code and public field; they exclude resource identities and
arbitrary diagnostics.
Rebuild after an SDK contract change
Section titled “Rebuild after an SDK contract change”Every Net Operation Project artifact records the gateway contract of the SDK it was built with, and the host runs only artifacts of its own contract. The check is exact equality: after an upgrade that changes a contract, the host refuses artifacts built for the older one, and an older host refuses artifacts built for a newer one. Nothing falls back or is skipped silently. Until the artifact is rebuilt, its function runs, commands, HTTP endpoints, Action launches and data sources fail, its event consumers stop, and Entity saves with its C# Validation or BeforeSaveMutation bindings fail.
The current SDK uses Net Operation Project gateway contract 13. SDK 4 moves
the shared DSL to a separate assembly, so generated projects must rebuild their
module dependencies as well as their own source. A full
moltaroctl upgrade rebuilds and activates the
working revision of the Net Operation Project, so an unpublished draft in that
revision becomes the active version; a manual Build replaces the active
artifact the same way. Publish or discard unpublished changes before upgrading.
Delta updates do not rebuild the project: for an upgrade that changes the
gateway contract, arrange a full upgrade with the administrator. Work queued
or leased with the old artifact
can fail after its retries; the release notes of a version that changes a
contract list the affected work and the order of the steps.
Reporting data sources
Section titled “Reporting data sources”Derive a provider from DataSource<TParameters> and declare its stable identity,
default name and description with [MoltaroDataSource]. Workspace-specific
providers belong under src/DataSources/ in the Net Operation Project. Describe
typed parameters, output schema, renderers and execution limits in Describe;
keep data selection in ExecuteAsync and parameter choices in GetOptionsAsync.
SDK 6 removes RequirePermission, MinimumRole and the execution-requirements
envelope. Technical module dependencies remain supported. After publishing,
an Owner, Administrator or Configurator assigns Use source to users or roles
in Data Sources administration. Unconfigured sources deny use, including to
administrators. Catalog text overrides and categories need no rebuild, and
Report/Dashboard visibility remains a separate setting. Source-use grants do
not replace business-record authorization inside the provider.
Series and shares
Section titled “Series and shares”SDK 6.1 lets category and time series sources name a series per row and a share of each value. Describe the fields with their display names and units; the builder assigns their roles:
builder.OutputCategorySeries( new DataSourceFieldDescriptor("Type", DataSourceValueTypeEnum.String, DataSourceText.FromString("Task type")), new DataSourceFieldDescriptor("Hours", DataSourceValueTypeEnum.Number, DataSourceText.FromString("Hours"), Unit: "h")) .Series( new("State", DataSourceValueTypeEnum.String, DataSourceText.FromString("State key")), new("StateLabel", DataSourceValueTypeEnum.String, DataSourceText.FromString("State"))) .Share(new("Share", DataSourceValueTypeEnum.Percentage, DataSourceText.FromString("Share"))) .Renderer(DataSourceRendererKindEnum.Table);Return one row per category (or time bucket) and series, sorted the way
viewers should read them; series appear in the order of their first row and take
theme colors in that order, so return every series in a fixed order, including
zero rows. Category names must be unique unless you declare
CategoryKey(new("WorkerId", DataSourceValueTypeEnum.String, ...)): then two
categories with the same name, such as two Workers, stay apart. Leave an unknown
value null rather than 0. A share is a fraction from 0 to 1 that your
source calculates, with a denominator it chooses; reports show it in the share
(100 %) bar view next to the absolute value and never recalculate it. Emit daily
buckets as the local midnight of the day in the execution context’s
TimeZoneId, with that day’s offset. Sources with a series are not offered as
donut charts. Write new DataSourceFieldDescriptor(...) for the two output
fields: a target-typed new(...) is ambiguous with the key overloads.
Row links to Tasks
Section titled “Row links to Tasks”A table or record list row can open a Task in its preview drawer. Declare the visible field and the field holding the Task id, and name the visible one in a drilldown; the Task id field is never shown as a column:
builder.OutputTablePage( new DataSourceFieldDescriptor("Task", DataSourceValueTypeEnum.String, DataSourceText.FromString("Task"), LinkRole: "TaskId"), new DataSourceFieldDescriptor("TaskId", DataSourceValueTypeEnum.String, DataSourceText.FromString("Task id"))) .Drilldown(DataSourceLinkTargetKindEnum.Task, "Task");A drilldown field without a link role, or whose link role names no declared field, links by its own value. A row whose Task id is null shows plain text. Opening a Task checks the viewer’s Task access again; a report never grants access to a Task.
Rebuild and republish older artifacts with SDK 6 (Package gateway 16 and Net Operation Project gateway 13). SDK 6.1 keeps both gateway contracts. The host keeps their saved catalog settings as unavailable until a compatible provider with the same identity is published; it does not convert old C# access declarations into grants.
Collectible runtime safety
Section titled “Collectible runtime safety”Package and Net Operation Project assemblies execute in collectible runtime
epochs. Keep work inside the invocation: use ordinary awaited async calls, and
use Moltaro schedules or IFunctionQueue when work must run later. Check and
Build reject detached, started, discarded, or unobserved stored tasks, threads
and ThreadPool work (MOLTARO040),
long-lived timers or callbacks (MOLTARO041), subscriptions to host events
(MOLTARO042), and explicit default-context/dynamic-load or GCHandle escapes
(MOLTARO044). CancellationToken.Register and UnsafeRegister are supported
only when the returned registration itself is disposed by using. A local
method-group delegate to either registration API is allowed only when every
invocation is disposed this way and the delegate does not escape.
Process-global ActivitySource.AddActivityListener registration and access to
DiagnosticListener.AllListeners are rejected. Creating, subscribing to, or
deriving from DiagnosticListener, and deriving from EventListener, are also
rejected because their process-global registration cannot be made fail-closed
in package code.
Call MeterListener.Start only on a receiver declared by using var or a
using declaration. A method-group delegate to Start must remain local and be
invoked directly.
A stored task must be directly awaited or returned by the very next statement;
for parallel work, store a combined Task.WhenAll(...) task and await it next.
MOLTARO043 warns on every user-authored non-const static/static-readonly
field. Review it as epoch-owned state: it must not escape to host state or
retain callbacks, host services, collectible CLR types, delegates, or
invocation values. Generated code and constants are excluded. Task.Delay and
ordinary async/await are supported. An already active artifact is not stopped
by a newly introduced analyzer rule, but its next Check or Build must pass the
current contract.
Reflective host-event subscriptions, AppDomain.SetData, AppContext.SetData,
process-principal replacement, named/thread-local data slots, and writes to
external static fields or properties are lifetime escapes and are rejected as
well. Process-global Encoding.RegisterProvider registration is also rejected.
The same applies to TypeDescriptor.AddProvider, AddProviderTransparent,
AddAttributes, AddEditorTable, and CreateAssociation.
The same rule rejects process-global Console.SetIn/SetOut/SetError
replacement and all Trace.Listeners collection access because those APIs can retain
package-owned readers, writers, or listeners after the runtime epoch. Taking a
method-group delegate to a forbidden process-global API is rejected too.
Structured result helpers for command and function data,
typed HTTP JSON, and data sources use the current runtime epoch’s serializer
context automatically. Existing installed artifacts that explicitly pass
process-singleton JSON options to those helpers remain runtime-safe because the
epoch context replaces them; their next Check/Build rejects that reference with
MOLTARO044, so omit the option. Direct JsonSerializer calls must pass a
freshly constructed inline options instance or explicit typed JSON metadata;
options obtained through parameters, fields, properties, or locals are
rejected because their ownership cannot be proven. A JsonSerializer method
group is allowed only when its selected overload requires JsonTypeInfo or
JsonSerializerContext. Do not put serializer
options, converters, runtime types, exceptions, or result objects into static
or other process-global storage.
Dynamic invocation is rejected because Check/Build cannot verify that the
runtime-selected target respects these lifetime rules.
Inline Activator.CreateInstance<T>() and CreateInstance(typeof(T)) remain
available for ordinary non-listener types. Type-erased activation and
Activator.CreateInstance method groups are rejected because the constructed
lifetime cannot be verified.
Parent assignment and reparenting use the same supported record write services and optimistic concurrency as any Reference. See the Parent Tree View developer guide.
Reference Eligibility is likewise configured on Entity Definition fields or through the Configuration API, not authored from public Net Operation Project schema code. The configuration workflow supports validation, impact preflight, activation, Form/Table pre-filtering, and authoritative save enforcement. See the Reference Eligibility guide.
Hierarchy Selector configuration is available from Entity Definition Form and Table-filter authoring and from the Configuration API. It supports fixed Reference paths and recursive indexed self-References while storing only the terminal Reference value. See the Hierarchy Selector guide.
Entity UI layout remains workspace configuration rather than Net Operation Project source. Configure responsive Card item width in the Entity Definition Surface library or through the Configuration API, as described in Card item layout. Workspace C# consumes the generated entity contract but does not author or override these UI spans.
Entity Manual Order is also platform-owned configuration. Net Operation Project source does not declare the capability, choose or write its protected rank storage, or receive a direct Manual Order move service. Generated entity writes continue through the normal Moltaro save pipeline. When Manual Order is active, that pipeline automatically gives each newly inserted active record a canonical tail position in the same transaction, so ordinary generated-context creates do not need special ordering code. Positional moves remain an authenticated application/UI operation with the same authorization and concurrency boundary as other Entity actions.
For workspace-specific algorithms that need their own ordered keys, the SDK
also exposes the policy-free
Moltaro.Package.NET.ModuleRuntime.Ordering.LexicographicRankKeyGenerator.
It returns typed generation statuses and evenly spaced sequences. Using that
primitive does not grant access to Moltaro’s protected Manual Order rank, and it
does not provide a Manual Order move endpoint or service.
Work Schedule application automation
Section titled “Work Schedule application automation”For calendar lifecycle, effective-dated Site and Worker assignments, schedule
exceptions, and effective-schedule reads, prefer
IWorkScheduleAutomationCommandService and
IWorkScheduleAutomationQueryService. The generated workspace contract
registers both interfaces, and developer-surface marks them
ApplicationAutomation and Preferred: true.
These calls run complete Work Schedule application orchestration in a separate
host-owned scope. Requests cannot supply an actor or authority tier. Every
command uses a stable caller-owned OperationId; calendar mutations additionally
use the current RowVersion, while assignment and exception mutations use the
last returned ChainVersion. Preserve an operation id only for an exact retry
after an unknown outcome.
The lower-level services in Moltaro.Package.NET.WorkSchedule.Runtime remain
available as advanced TrustedDirectDb, Preferred: false contracts for code
that explicitly owns authorization, transaction, audit, and side effects. Read
the Work Schedule C# guide before choosing
that boundary.
Generated entity search declarations
Section titled “Generated entity search declarations”The generated workspace contract reproduces each Entity Definition’s explicit PostgreSQL search targets through the SDK entity builder:
SearchField(...)declares a direct field or a path through Reference, Inverse Reference, or Table fields to a String, Text, Address, or File terminal;SearchSystem(EntitySearchSystemTargetEnum, ...)declares Display name, Number, Comments, or Attachments at the root or after a relation path.
These declarations are generated build input, not user-owned Net Operation Project source. Configure search targets in the Entity Definition Search tab or through the Configuration API search contract; the next generated contract then gives Check, Build, and server-side language services the same schema metadata.
Generated String and Text properties whose Entity Definition field is labeled
with the Markdown property label still expose the original Markdown source as
their typed property value. Moltaro separately maintains a display-safe
plain-text projection for search, presentation, projections, and audit
previews. Net Operation Project code should write Markdown through the generated
context and its normal SaveChanges pipeline; do not issue raw SQL against the
source or its internal companion column. The save is rejected when the source
exceeds the 512 KiB UTF-8 limit, and source plus projection are committed
together.
A tracked direct-database update through MoltaroDbContext.SaveChanges is
authoritative for every modified field. The save removes stored declarative
mutation-effect ownership only from those fields and preserves ownership of
unchanged fields. It does not execute configured mutation effects; those run
only through the application/API Entity save pipeline.
Markdown inline images use opaque
moltaro-attachment://<FileReferenceId> source tokens. Package code must not
construct raw download URLs or treat a token as an access grant. The public
Net Operation Project programming model does not expose draft-session adoption;
use product record forms for destination-scoped image authoring.
All new attachment uploads enter PendingInspection. The descriptor exposes
AvailabilityState, detected MIME, image dimensions, content generation, and
the checksum/generation covered by successful inspection. Only Available
content whose inspected generation and checksum still match can be downloaded
through IAttachmentService. MoltaroAttachmentDownloadResult.Content is now
a non-null verified Stream; dispose it after reading. Moltaro verifies the
immutable body into a bounded spool before exposing any bytes, and the result
does not expose BlobUri. Attachment downloads are intentionally rejected
inside caller-owned database transactions so a checksum mismatch can persist
its security invalidation independently. Code compiled against the previous
buffered result must be updated before moving to this SDK contract.
For large Managed Cloud attachments, use UploadStreamAsync or
UpsertStreamAsync with an exact declared size and a stable idempotency key.
Call GetUploadCapabilitiesAsync before opening or downloading the source. It
returns the effective storage type, maximum file size, multipart availability,
part policy, checksum policy, actor-local active-session/reserved-byte usage
and limits, any saturation retry delay, the 24-hour inactivity and 48-hour
absolute session lease, and authoritative extension-aware FileTypeRules plus
FileTypePolicyVersion and FileTypePolicyHash without
exposing storage credentials. A rule accepts a file
only when its longest matching extension and normalized declared MIME type are
present together. AllowedMimeTypes is a deprecated, conservative compatibility
projection and must not be used to make new upload decisions. It deliberately
omits application/octet-stream; a pre-rule runtime may reject DWG/FBD after a
rollback instead of allowing ambiguous bytes for every extension. A pre-session
policy rejection throws MoltaroSdkException; inspect its stable FailureCode
(also available as ReasonCode) instead of parsing the safe bounded message.
When FailureCode is filesystem.upload.activeLimitReached, the exception has
IsRetryable = true, exposes RetryAfterSeconds, and includes safe active
count/limit and reserved-byte usage/limit metadata. Keep the idempotency key
unchanged and retry only after the advertised delay.
filesystem.file.type.notAllowed metadata includes the normalized extension,
declared MIME, policy version, and policy hash so a caller can correlate the
decision with the exact preflight snapshot across API and Worker processes.
The rule check is not antivirus or malware inspection and cannot detect an
executable file renamed to an allowed ambiguous format. Workspace administrators
may deliberately configure executable extensions and own the consequences of
that policy.
The source may be non-seekable; Moltaro reads it in bounded parallel windows of
16 MiB parts up to the advertised MaximumParallelParts and does not expose S3
endpoints, buckets, object keys, credentials, or presigned URLs. Interactive
cancellation preserves the resumable session. Host
cancellation of a trusted background function aborts that physical attempt and
releases capacity; an exact retry with the same logical idempotency key creates
a successor. Retry the complete stream with the same key, poll
GetUploadAsync, and call AbortUploadAsync only when the upload should be
discarded. Uploaded means the provider accepted the
body, Verifying means background verification/publication is running, and
only Available returns the published reference and descriptor ids.
The idempotency key and requested file-reference id identify one logical
upload; UploadSessionId identifies its current physical attempt. If the
attempt expired, repeat the same stream call with a fresh stream and unchanged
actor, owner, metadata, key, and file-reference id. Moltaro returns one new
successor session after rechecking current policy and concurrency. Querying the
old session still returns terminal filesystem.upload.expired. A background
attempt aborted by host cancellation may restart; explicitly aborted ordinary
uploads and rejected uploads remain terminal. An already published result is
replayed without uploading again.
Failed with filesystem.upload.providerUnavailable is non-terminal; reopen
the source and repeat the same stream call with the same idempotency key because
polling does not retry a provider command.
Start, status, abort, and download operations must run outside an ambient
package database transaction because they can perform external storage I/O.
The legacy direct-database Database/Local upload and upsert methods remain
available and enforce the same workspace FileTypeRules before accepting
bytes.
For a migration backlog, use UpsertStreamsAsync rather than running concurrent
calls through one scoped service instance. A batch accepts at most 100 items;
MaximumConcurrency is 1 through 32 and defaults to 4, while current actor upload
capacity may reduce the effective concurrency. Ordinary actors remain capped at
8 active sessions; only host-established trusted system automation receives the
dedicated 32-session capacity. Each item owns an independent
runtime scope and transaction, results stay in input order, and one item failure
does not cancel successful siblings. Each failed result carries its stable code
and retry metadata. If a terminal item failure occurs after session creation,
the runtime aborts the physical attempt and releases its reservation before
returning the result; retryable failures retain their resumable attempt. Provide
a separate caller-owned readable stream for every
item and invoke the batch outside an ambient package transaction. A successful
item has entered the transfer lifecycle; poll its returned upload-session id to
Available before archiving the source migration task.
Trusted scheduled code may use either stream method when the historical author
is unknown. UploadStreamAsync has no source-author input; for
UpsertStreamAsync, leave SourceCreatedByUserId null. Moltaro preserves null
on the published descriptor, file reference, and relations while keeping the
runtime-owned moltaro-system-automation identity as the authorization and
audit actor. A non-null source creator must resolve to an existing workspace
user and never grants access. Direct-db upload/upsert follows the same unknown
creator rule for the reserved system actor; ordinary direct-db calls require a
real actor and attribute an omitted creator to that actor.
Use IAttachmentService.ReassignAsync to move one published attachment between
EntityInstance owners without changing its FileReferenceId or copying its
body. The request carries a stable OperationId, exact source and target owner
identities, and the row version returned by the attachment list. A committed
retry returns AlreadyApplied; the descriptor, provider object, bytes,
checksum, upload attribution, and relations stay unchanged. Moltaro rejects the
operation when access or Business Invariants deny detach/attach, the reference
is still used by a File or nested Table<File> field, its inspected content
is not currently downloadable, an upload/replacement or hold is active, or the
target already contains the normalized filename. Use the stable
filesystem.reassign.* failure code rather than parsing the safe message.
When a configured Business Invariant provides its own stable rejection code,
Moltaro preserves that code so the caller can identify the exact policy that
blocked the reassignment; filesystem.reassign.invariantRejected is the
fallback when the invariant provides no code.
Runtime functions require the Moltaro host gateway and never call their own HTTP
API or receive provider locators. A direct-db host must configure a canonical
workspace name and a real Business Invariant guard, and must call reassignment
outside any ambient package transaction.
This large-file contract does not change Markdown inline images. Markdown draft
uploads remain image-only, at most 25 items and 100 MiB in aggregate, and use
opaque moltaro-attachment:// tokens only after the existing draft workflow.
Do not place a presigned URL in Markdown source.
Comment imports, reconciliation, and reassignment
Section titled “Comment imports, reconciliation, and reassignment”Use ICommentService.UpsertAsync for an import or restore that must preserve a
comment id, source creation time, source author, and relations. It cannot move
an existing comment to another owner. If the source author is omitted in a
host-authenticated CRON or Trigger function, Moltaro records
moltaro-system-automation; an explicit source author must be an existing
workspace user. The reserved actor id by itself is not trusted provenance.
When the same source comment can move between records after a previous import,
use ReconcileAsync from a host-authenticated CRON or Trigger function. The
operation locks the source-stable primary id and a distinct owner-scoped
fallback id in one transaction. It creates, reconciles, or restores a primary
that is absent or already belongs to the target; an active primary under a
different owner moves in place. A deleted primary under a different owner is
kept as an audit tombstone, so the operation creates, reconciles, or restores
the fallback instead. Moltaro durably binds the stable primary id to the current
effective comment. On a later owner change, an active bound fallback moves in
place and the result is FallbackReassigned; it can therefore differ from the
new owner-scoped fallback supplied by that request. If the bound effective
comment was deleted, its tombstone remains and the current owner-scoped fallback
is selected instead. The binding survives worker restarts and serializes
competing owner changes, so one primary identity has at most one active logical
comment. Missing or incompatible binding evidence fails closed.
Primary-preserving branches take precedence: if their
fallback is already active, the operation fails closed as an ambiguous
duplicate instead of leaving two active logical comments. During an unbound
first reconciliation, same-owner fallback reconciliation or restore is valid
only after a foreign primary tombstone selects the fallback branch. A later
deleted bound effective comment also selects the supplied fallback as described
above. Always persist the typed result’s EffectiveCommentId.
The example below shows the complete identity and retry protocol. The queue stores a fresh operation id before the first call and reuses it only while the outcome is unknown. A later reconciliation cycle gets a new operation id even when source content is unchanged, so it can repair a target comment that was subsequently deleted. Primary and fallback ids remain deterministic across all cycles. Treat the returned effective id as authoritative; never assume that the fallback supplied to the current call won.
using System.Security.Cryptography;using System.Text;
static string StableCommentId(string prefix, params string[] parts){ var canonical = string.Join( "\n", parts.Select(part => $"{Encoding.UTF8.GetByteCount(part)}:{part}")); var digest = Convert.ToHexString( SHA256.HashData(Encoding.UTF8.GetBytes(canonical))) .ToLowerInvariant(); return $"{prefix}-{digest[..40]}";}
var targetOwner = MoltaroResourceIdentity.BoardItem(boardId, boardItemId);var primaryCommentId = StableCommentId( "legacy-comment", legacySystemKey, legacyComment.Id);var fallbackCommentId = StableCommentId( "legacy-comment-owner", legacySystemKey, legacyComment.Id, targetOwner.ModuleKey, targetOwner.ResourceKind, targetOwner.ContextResourceId, targetOwner.ResourceObjectId);
// Persist before calling. Reuse only after a timeout or another unknown outcome.var operationId = queueItem.PendingOperationId ?? Guid.NewGuid().ToString("N");await queue.SavePendingOperationAsync(queueItem.Id, operationId, cancellationToken);
var reconciled = await comments.ReconcileAsync( new MoltaroCommentReconcileRequest { OperationId = operationId, PrimaryCommentId = primaryCommentId, FallbackCommentId = fallbackCommentId, TargetOwner = targetOwner, CommentText = legacyComment.Text, SourceCreatedByUserId = mappedWorkspaceUserId, // Omit for trusted system-automation attribution. SourceCreatedAt = legacyComment.CreatedAt, QuotedCommentId = mappedQuotedCommentId, Relations = relations }, cancellationToken);
await queue.MarkCommentReconciledAsync( queueItem.Id, reconciled.EffectiveCommentId, reconciled.RowVersion, cancellationToken);Exact replay returns the original outcome, effective id, and row version with
IsIdempotentReplay = true and performs no comment, relation, receipt, or event
write. Reusing one operation id with changed content, owner, author, timestamp,
quote, or relation set fails with ReconcileOperationConflict. An incompatible
fallback binding fails with ReconcileCandidateConflict instead of adopting or
disclosing the other comment. User-action functions and external direct-db
hosts cannot invoke reconciliation.
Once an id appears as a primary, fallback, or effective identity in durable
reconciliation evidence, general UpsertAsync and independent ReassignAsync
calls reject it with ReconciliationIdentityReserved. Keep that logical import
on ReconcileAsync. Ordinary update/delete calls share its identity lock and
authorize the current owner inside their transaction, so a concurrent owner
move cannot apply a mutation authorized against the previous owner.
For a deletion decision based on a read, inspect current.ActiveRelations as
well as content and attribution, then use the conditional overload:
var current = await comments.GetAsync(commentId, cancellationToken) ?? throw new InvalidOperationException("The comment no longer exists.");
// Apply your application's preservation checks to this exact snapshot first.await comments.DeleteAsync( new MoltaroCommentDeleteRequest(current.Id, current.Owner, current.RowVersion), cancellationToken);The owner and non-empty version are checked inside the normal deletion
transaction. A conflict leaves the comment and its relations intact; re-read
and reconsider the decision. The string overload deletes current state and
does not bind deletion to an earlier inspection. These overloads keep the same
authorization rules and are available through runtime and trusted direct-db
services. ActiveRelations contains all active relation snapshots in service
reads; custom EF queries do not load relations automatically. The token covers
the comment, not another canonical comment, a board target, or file lifecycle.
For cleanup that requires no file links, inspect the read-only comment/file link
projection too: the service’s Files hides deleted, potentially restorable
placements even when an active link remains.
CRON and Trigger functions can use ReassignAsync to atomically move one active
comment between existing EntityInstance or BoardItem owners. The comment id,
text, quote, source attribution/time, and relations remain unchanged. Supply a
stable operation id and the current comment row version. Replaying that exact
request returns AlreadyApplied without duplicate writes or events. User-action
functions and external direct-db hosts cannot use this operation, and a
reconciliation-reserved comment must remain on the reconciliation path.
var current = await comments.GetAsync(commentId, cancellationToken) ?? throw new InvalidOperationException("The source comment no longer exists.");
var moved = await comments.ReassignAsync( new MoltaroCommentReassignRequest { OperationId = operationId, CommentId = current.Id, SourceOwner = MoltaroResourceIdentity.BoardItem(sourceBoardId, sourceItemId), TargetOwner = MoltaroResourceIdentity.BoardItem(targetBoardId, targetItemId), ExpectedRowVersion = current.RowVersion }, cancellationToken);Keep the operation id only for the exact retry. On stale input, re-read and
start a new operation. Handle MoltaroSdkException.ReasonCode using
MoltaroCommentErrorCodes; do not parse error messages.
Principal CRUD and application automation services
Section titled “Principal CRUD and application automation services”For workspace C# that changes principals, use the existing principal CRUD services. Boards and Entitlement Operations retain their curated application-level services:
IUserService,IRoleService, andIGroupService;IBoardAutomationCommandServiceandIBoardAutomationQueryService;IEntitlementAutomationCommandServiceandIEntitlementAutomationQueryService.
Principal mutations create their own transaction, serialize with authoritative Entity authorization through the workspace access-state lock, and atomically commit the principal rows, cache invalidation version, and applicable user-index outbox row. They are not supported inside an already active package transaction.
MoltaroRole.RowVersion versions the complete permission set, including an
empty set. IRoleService.ReplacePermissionsAsync requires that value in
MoltaroReplaceRolePermissionsRequest; stale input fails with
roles.permissions.staleRowVersion without changing state, while exact replay
is write-free. Scheduled jobs and other hosted Package or Net Operation Project
functions can inject IRoleService directly. Permission changes validate against
the installation’s current permission catalog, including dynamic permissions;
your startup does not need to register a permission directory.
Prefer atomic GrantPermissionAsync and
RevokePermissionAsync for additive integrations. External direct-db hosts
must populate MoltaroCoreServicesOptions.KnownPermissionKeys; the default
empty catalog and unknown keys fail closed.
Entity application automation uses IEntityAutomationCommandService. Its exact
15 commands are CreateAsync, UpdateAsync, ArchiveAsync, RestoreAsync,
DeleteAsync, MergeAsync, ImportAsync, BulkUpdateAsync,
ReplaceResponsibilitiesAsync, ReplaceTagsAsync, CreateCommentAsync,
UpdateCommentAsync, DeleteCommentAsync, UploadAttachmentAsync, and
DeleteAttachmentAsync. Every request carries an OperationKey; exact replay
returns the stored authorized result, while reusing the key for different input
returns an idempotency conflict. Import and bulk update accept at most 100 items
per captured transaction. UpdateAsync, bulk-update items and import update
items follow the HTTP PATCH rule: an item that writes a field creates a new
record version even when its values equal the stored ones, so send only changed
fields (see
Update with optimistic concurrency).
For record import and field history, start with Import records and retain history. It compares application commands with direct EF, explicit business events and historical audit import. The command operates on one definition; outer import success does not mean every item succeeded. The guide includes a compile-checked example and transaction/replay limitations.
Create, update, import and bulk-update items record the function run that
issued the command as the change source
of each history entry: Function, Trigger for a trigger function or
Schedule for a scheduled run, with the executed version and the user who
started the run when there is one. These writes do not start Entity trigger
functions of the records they change, so call any follow-up logic explicitly.
CreateAsync takes the five-member EntityCreateCommandRequest: a non-empty
UUID OperationKey, the matching EntityDefinitionKey, a Values field map,
the latest ExpectedDefinitionRowVersion, and a non-null
ResponsibilityCandidates list. Use
Array.Empty<ResponsibilityCandidatePayload>() when the caller proposes no
StageOnCreate assignments. Preserve the operation key only for an exact retry;
re-read the Entity Definition before rebuilding a request after a schema
concurrency conflict.
Code written for the earlier create shape must replace Fields with Values
and replace string MutationIdempotencyKey values with caller-generated UUID
OperationKey values. Rebuild against the current SDK rather than maintaining
an adapter for the removed request shape.
Each method returns SecurityMutationCommandResult<TResult>. Success values are
the documented authorized-detail, safe identity/receipt, responsibility, tag,
comment, attachment, import, or bulk result unions. A committed Entity record
mutation whose result cannot be shown answers EntityRecordMutationReceipt, and
a responsibility replacement EntityResponsibilityMutationReceipt; both derive
from MutationReceipt, carry a typed CallerKnownIdentity, and keep the JSON
discriminator "Receipt". Tag, comment, and attachment mutations still answer
ResourceMutationReceipt, so match each union on its own receipt type. Current
Entity View and field visibility are rechecked before replay output, so a retry
cannot reveal a record or field that is no longer readable; such a replay answers
a receipt with IsIdempotentReplay = true, and a Create replay fails with
moltaro.instances.replay.resultNotViewable because its record identity was never
known to the caller.
The typed receipts came with Package gateway contract 12 and Net Operation
Project gateway contract 9; the current SDK uses 16 and 13. Rebuild and
republish every package and project against the current SDK: the host refuses an
artifact built for another contract instead of running code whose receipt checks
would silently stop matching (see
Rebuild after an SDK contract change). Search your code for
ResourceMutationReceipt rather than relying on the compiler alone: a plain
is ResourceMutationReceipt test on an Entity result compiles with only a warning
and is always false. This facade is host-enforced and
always requires the current Entity Security configuration and dependency
guards. Its host-authenticated system actor bypasses end-user permission
statements but not configuration existence, lifecycle invariants, concurrency,
or idempotency; the reserved actor id without trusted provenance grants no
access. It is distinct from trusted direct-database services, which must never
be substituted behind an application-facade call.
For record lifecycle automation, inject IEntityAutomationCommandService and
pass EntityIdentityCommandRequest to ArchiveAsync or RestoreAsync:
var request = new EntityIdentityCommandRequest( operationKey, "service_ticket", ticketId, currentRowVersion, "Closed by retention policy");
var result = await entities.ArchiveAsync(request, cancellationToken);if (!result.IsSuccess){ var codes = result.Errors.Select(error => error.Code).ToArray(); return FunctionResult.Failed(string.Join(", ", codes));}
var receipt = (EntityLifecycleMutationReceipt)result.Result!;var nextRowVersion = receipt.Data.RowVersion;var replayed = receipt.IsIdempotentReplay;Preserve operationKey only for an exact retry and use the record’s current
RowVersion. moltaro.instances.notFound is the safe missing-or-not-visible
failure, moltaro.security.concurrency.staleRowVersion (Field RowVersion)
means the row version is stale, moltaro.instances.conflict means that a
concurrent write holds the record, and
moltaro.instances.archive.alreadyArchived / moltaro.instances.restore.notArchived
report lifecycle-state conflicts. moltaro.instances.reason.required and
moltaro.instances.reason.tooLong (Field Reason) reject a missing, blank or
longer than 1000-character reason before any write; the trimmed reason is
stored with the operation and shown in record history. The installation’s
GET /api/workspace/error-catalog describes the REST answers of these
operations; C# command results are not covered by it and can report other
codes for the same condition, for example moltaro.security.request.invalid for
a malformed command instead of the REST request codes. Success always returns an
EntityLifecycleMutationReceipt; its identity-only Data contains the new
committed RowVersion without disclosing Entity field values. The receipt
exposes IsIdempotentReplay, and a replay does not emit another audit row or
resource event. Permanent DeleteAsync applies the same reason rules.
MergeAsync requires EntityMergeCommandRequest.Reason with the same bounds
and stores it with the archive of every merged source record. The effective
actor of this facade is always
moltaro-system-automation. Scheduled Jobs may run without an original user;
when an interactive user exists, that identity and the function, run,
correlation, and Schedule/Trigger/Function origin remain audit attribution.
ReplaceResponsibilitiesAsync replaces the complete assignment timeline of one
responsibility on one record. Current subjects that are left out of Subjects
are closed; keep the EffectiveFrom of subjects that stay, because a new
instant closes and reopens their assignment.
EntityReplaceResponsibilitiesCommandRequest.RowVersion is the record’s
responsibility state version, ResourceRowVersion. All responsibilities of the
record share it, and it is not the record’s row version. A record without any
responsibility state (none of its responsibilities was ever assigned, including
at creation) reports the empty UUID, so its first assignment passes
Guid.Empty:
var request = new EntityReplaceResponsibilitiesCommandRequest( operationKey, new ResourceIdentity( MoltaroResourceModuleKeys.CoreEntity, MoltaroResourceKinds.EntityInstance, definitionId, ticketId), "Owner", [new ResponsibilitySubjectPayload("User", ownerUserId, DateTimeOffset.UtcNow, null)], Guid.Empty, "Assigned by intake automation");
var result = await entities.ReplaceResponsibilitiesAsync(request, cancellationToken);Afterwards pass the ResourceRowVersion of the latest replace result on that
record, for any of its responsibilities. The C# facade has no read for this
version; when the record already has state that your code did not write, such
as assignments made at creation, read it from
the current responsibility state
over HTTP. A value other than the current one, including Guid.Empty once the
record has responsibility state, fails without changing the assignments;
re-read the state and start a new operation. The reason must contain 1–1000
characters after trimming. An exact retry repeats the same request, including
its EffectiveFrom values, and returns the stored state without another write.
Reusing the key for a different request on the same record and responsibility
fails with moltaro.security.idempotency.keyReuse.
Board Statements are workspace configuration authored through the Boards Configuration API, not a new C# evaluator or Net Operation Project service. Their public Boards package schema exists for trusted direct-database module integration, while supported workspace logic should use the documented configuration endpoints and server-owned DSL authoring profile.
Board automation facade mutations enforce enabled transition, entry, exit, and
destination status-invariant Constraints through the same application
transaction as the Runtime API.
BoardAutomationMoveValidationResult exposes ordered Errors,
CapturedAtUtc, and ConstraintDecisions in addition to compatibility
ErrorCodes; callers must handle every returned error and must not parse the
workspace-authored message. The advanced direct-database Boards package also
exposes IBoardConstraintRuntimeEvaluator and
AddMoltaroBoardsConstraintRuntimeEvaluator<TEvaluator>() for trusted hosts.
This public custom-evaluator contract handles Board events only. It cannot opt
a standalone host into proposed-state StatusInvariant evaluation, which
remains fail-closed outside Moltaro’s host-provided integration. Its default
event evaluator fails closed if an enabled Binding applies without a registered
evaluator. Destination pickers should call
IBoardRuntimeCommandService.ValidateMoveOptionsAsync; it evaluates candidates
for one item and actor as one request-local batch. Custom high-volume evaluators
should override EvaluateBatchAsync. Authoritative mutations call
EvaluateAuthoritativeBatchAsync only after acquiring governance locks. An
evaluator backed by a configuration or compiled-plan cache must override
that method and reload the applicable configuration inside that call; it may
delegate to EvaluateBatchAsync only when its evaluation source is uncached and
already authoritative. Custom definition providers must implement the remaining
set-based Board definition lookups. BoardRuntimeActor carries only the user id;
roles, groups, responsibilities, Statements, and exact transition permissions
come from the host’s captured SQL authorization scope.
Direct package move validation also returns ordered transition
LinkPrerequisites with current and missing counts. A trusted host can set
MoveBoardRuntimeItemRequest.PrerequisiteLinks to existing Board item ids;
the package derives link kind/direction from the transition, applies ordinary
link permissions and invariants, and commits any new link with the move. Inline
target creation remains an application/Runtime API concern rather than a
direct-package contract.
Composite Entity mutations as the current user
Section titled “Composite Entity mutations as the current user”IEntityCompositeMutationService creates and updates standalone Entity
records, up to the host’s step limit (100 by default), in one transaction: every step commits or none does. Unlike
IEntityAutomationCommandService, it runs as the user the host admitted for
the current run, with that user’s Entity Security, and never as
moltaro-system-automation:
- a Command runs as the user who invoked it;
- an HTTP endpoint runs as the bearer user (
AuthenticatedUser) or the configured Service user (ConfiguredServiceUser); - Jobs, schedules, triggers, Actions and other runs without an admitted current
user are refused with
moltaro.automation.entity.currentUserInvocationRequired, and a call while the function’s database context has an open transaction withmoltaro.automation.entity.ambientTransactionUnsupported.
ExecuteAsync takes an EntityCompositeMutation with a caller-owned
OperationKey and ordered EntityCompositeCreateStep and
EntityCompositeUpdateStep items, and returns
SecurityMutationCommandResult<EntityCompositeMutationResult> with one
EntityCompositeRecordIdentity or EntityCompositeRecordReceipt per step.
Errors name the step in Field (such as Steps[1].RowVersion) and in
Metadata.StepId. The steps, the host’s limits, replay and history follow the
REST operation; EntityCompositeMutationLimits holds the limits every host
accepts (16 steps, 1,048,576 bytes), not this host’s own; the same user’s REST request and C#
call share the OperationKey journal. Writes of this service do not start
Entity trigger functions. The Net Operation Project registers the service
automatically. The rules, a compile-checked Command and the error handling are
in Composite Entity mutations.
Generated-context transaction writes
Section titled “Generated-context transaction writes”When code opens a generated-context transaction directly, call
AcquireMoltaroDynamicSchemaReadLockAsync immediately after
BeginTransactionAsync and before any query, row lock, or mutation. Use the
synchronous pair for synchronous transactions. The lock prevents a NetPackage
schema update from changing the meaning of the generated EF model during the
unit of work. ExecuteInTransactionIfNotExistsAsync takes this lock
automatically before it invokes package code.
Normal generated-context SaveChanges and SaveChangesAsync also enforce
status invariants when an existing Entity is the Target or Board Data of a
governing Board Item. Moltaro evaluates the complete tracked proposed root and
owned-table state, including owned Address and Money changes, before persistence.
Single Select values use one option key and Multi-Select values use an array of
option keys. The generated workspace contract maps a sanitized C# enum member
back to the actual workspace option key through a platform-owned EF converter;
for example, member InProgress can represent key in_progress. Board DSL and
Runtime API values use the workspace option key, not the sanitized member name.
Generated String fields carrying the TimeZone semantic role retain that role in
direct-database audit evidence. Normal generated-context SaveChanges and
SaveChangesAsync canonicalize IANA aliases and reject values outside the
platform catalog before persistence, including owned table rows. Raw SQL bypasses
this protection and is not a supported mutation path.
File values use stable string ids. Upload directly through
IFileFieldService, wait for Available, and then save and read back the
field reference. IAttachmentService owns the separate resource attachments
path. See File fields and attachments
for scheduled imports, destination permissions and replay.
Money uses an Amount/CurrencyCode object:
a missing Amount makes the whole value canonical null, while an omitted,
null, empty, or whitespace CurrencyCode uses the field’s normalized uppercase
BaseCurrencyCode. An Address with no meaningful text or coordinate component
is canonical null. Persisted and proposed values use the same carriers as the
Runtime API. A rejection rolls back the save and
throws MoltaroPreCommitSaveRejectedException; its Errors collection
contains the safe, ordered failures from every registered provider, including
all governing active Boards. Catch that typed exception when function code
needs to translate a rejected save into its own result. Deleting a record that
another module still depends on, for example the Context of an unfinished task,
is rejected the same way: the dependent module’s errors are then followed by
moltaro.instances.delete.referenced.
The exception is public from
Moltaro.Package.NET.ModuleRuntime.Runtime. The following fragment is intended
for an OnRunAsync method that returns FunctionResult:
using System.Linq;using Moltaro.Package.NET.Functions;using Moltaro.Package.NET.ModuleRuntime.Runtime;
try{ await dbContext.SaveChangesAsync(cancellationToken);}catch (MoltaroPreCommitSaveRejectedException exception){ return FunctionResult.OkData(new { Accepted = false, Errors = exception.Errors.Select(error => new { error.Code, error.Target, error.Metadata, }), });}Branch on Errors[].Code, not Message. A run that does not catch the
exception receives the general run-level failure reason
moltaro.preCommit.rejected; the provider-specific reasons remain the ordered
codes in Errors because one save may be rejected for more than one reason.
Moltaro persists up to 50 ordered entries in the function Run detail, caps
FieldKey/Code at 128 UTF-16 code units and Message at 1024, caps the JSONB
envelope at 131,072 UTF-16 code units, and reports any clipping with
ValidationErrorsTruncated. Queued and scheduled rejections are terminal
Failed outcomes, so do not enqueue a replacement as a transport retry. The
Function operations Run drawer shows the retained entries and links to the
owning queued job. Entity-context diagnostics are projected through current
Entity row and field-read authorization, so restricted or unknown field entries
are omitted for callers who cannot read those fields. Fieldless entries require
visibility of every active field because no narrower field boundary is available.
The protected correlated DevLog event contains the same retained ordered
FieldKey/Code pairs plus counts and truncation, but never validation messages,
business values, SQL, secrets, stacks, or the raw exception.
BoardConstraintSaveRejectedException is a separate exception type used only
by direct Boards runtime commands and protected direct Board mutations; code
that handles both boundaries must catch both types explicitly. Multiple tracked saves inside
one explicit transaction retain the latest proposed Entity state from earlier
successful saves. The first Entity mutation lock batch fixes the transaction’s
complete Entity identity set, including a Board-command batch or a save later
rejected by validation. Every later batch must be a subset. A new identity fails
before lock acquisition with
moltaro.preCommit.newEntityIdentityAfterLockBatchUnsupported; stage all
records in a cross-Entity unit of work and save them in one call.
This lock-batch rule applies to every generated-context Entity transaction,
even when none of the records is used by a Board. The complete sorted identity
set is fixed before locks are acquired so concurrent transactions cannot each
hold one Entity lock while waiting for the other.
Manual Order adds the same fail-closed transaction rule for its separate lock.
If an Entity mutation lock has already been acquired, a later save cannot first
introduce a create, archive, restore, or delete that needs the Entity’s Manual
Order lock. The SDK throws MoltaroPreCommitSaveRejectedException before lock
acquisition with code
moltaro.preCommit.manualOrderAfterEntityLockUnsupported. Stage that lifecycle
operation in the first save, or roll back/complete the current work and retry it
in a new transaction. Catch the typed exception and inspect Errors; do not
parse its message.
New generated Entity ids are application-assigned strings. For an atomic
multi-record import, assign every id first, connect References/foreign keys in
memory, add the complete graph to the context, and call SaveChangesAsync
once. A dependent record does not need an intermediate save merely to discover
its parent’s id. Prefer source-stable ids or a durable source-id mapping when
the import must be safely repeatable. See
Create a related Entity graph in one atomic save.
A direct generated-context save is trusted code without Entity Security; to
save several standalone records atomically with the caller’s own access,
history and replay, a Command or HTTP endpoint uses
IEntityCompositeMutationService
instead.
An ordinary single-savepoint rollback restores that snapshot: Entity state
saved before the savepoint remains visible, while state saved after it is
discarded. If nested savepoint history is ambiguous to the runtime, every later
Entity save fails closed with moltaro.preCommit.transactionOverlayInvalidated; roll back
the transaction and retry the complete unit of work in a new transaction.
MoltaroDbContext.SaveChanges[Async] creates that savepoint before the
OnBeforeSaveChanges[Async] hook. Database work performed by the hook, runtime
preflight, the Entity write, mutation-effect ownership release, audit, and
runtime outbox changes therefore share the same rollback boundary.
An owned table row’s OwnerEntityInstanceId is immutable after insertion.
Do not parse exception text or expose evaluated field values.
The same save boundary validates newly assigned File fields on Entity roots and
table rows. The id must resolve to a non-deleted file reference and descriptor
whose content is Available or LegacyUninspected. Pending, rejected,
soft-deleted, and nonexistent references are rejected through
MoltaroPreCommitSaveRejectedException with the safe code
filesystem.file.content.unavailable. An unchanged existing value does not
block an unrelated edit.
ExecuteUpdate[Async] and ExecuteDelete[Async] over a MoltaroEntity or
one of its owned table-row types produce analyzer error MOLTARO039: EF bulk
operations bypass interceptors and therefore cannot participate in pre-commit
validation, audit, presentation, or runtime side effects. Direct SQL that bypasses the generated
MoltaroDbContext is unsupported.
Do not add, edit, or delete BoardItem rows through a generated context.
Tracked direct writes are rejected at save time; ExecuteUpdate[Async] and
ExecuteDelete[Async] over BoardItem produce analyzer error
MOLTAROBOARD001. Use IBoardRuntimeCommandService so locking, Constraints,
history, audit, and outbox behavior stay atomic. Raw SQL against Board runtime
tables is outside the supported Package SDK contract.
Inside one explicit package transaction, call a Board runtime command before
staging or saving any Entity or owned-table mutation. A direct Boards runtime
service call throws BoardConstraintSaveRejectedException, and a Net Operation
Project Board automation facade returns an error, with
moltaroBoards.runtime.entityMutationBeforeBoardCommandUnsupported when this
ordering is violated. The facade performs this check before opening its
separate application scope, so it cannot wait on locks held by the calling
transaction. Internal Moltaro orchestration may use configuration-prelocked
paths that are not part of the Package SDK contract.
Board configuration is admin-owned. Tracked direct writes to Board,
Statement, Constraint, Binding, Status, Transition, Target Definition, and
related configuration rows are rejected at save time. Bulk writes over those
types produce MOLTAROBOARD002. Use the Board Configuration API so advisory
locking, DSL validation, active-board health checks, and cache invalidation are
part of the same transaction.
Historical Board import and reconciliation do not replay transition, entry, or exit Constraints against the current actor or clock. Only the resulting status invariant governs that path, and it is enforced before commit.
Principal identities
Section titled “Principal identities”Principal CRUD accepts caller-supplied stable string ids. Use an external
source id as MoltaroCreateUserRequest.Id, MoltaroCreateRoleRequest.Id, or
MoltaroCreateGroupRequest.Id, call the matching Get*Async, then choose
Create*Async or Update*Async. These ids are the persisted Moltaro ids, not
separate external keys. There is no separate synchronization facade, upsert
result family, or preflight service.
The exact Unicode UserName is stored without trimming or rewriting; lookup
uniqueness uses trim + Unicode NFC + invariant uppercase. User create/update
can apply profile, type, email confirmation, blocked state, complete role
membership, and metadata in one operation. MoltaroUpdateUserRequest.RoleIds
is applied literally — an explicit list revokes every role missing from it,
including Admin and Configurator, while null preserves the current
memberships and Everyone is always retained. UserType is nullable and null
keeps the stored type. Credential activation remains explicit. Role and group
create/update use the same stable-id CRUD pattern, and built-in system role ids
cannot be created, renamed, or deleted.
These services signal failure with ordinary .NET exceptions —
ArgumentException for malformed input and InvalidOperationException for a
rejected operation such as a duplicate id, a protected role, or an Owner
change. They carry English messages and no stable code, so check state with the
matching Get*Async before writing instead of parsing exception text. The
stable ApiError codes documented for users and roles belong to the workspace
HTTP API.
ITagService.ReplaceAsync uses the current execution actor for newly added
tags. Scheduled automation is therefore attributed to
moltaro-system-automation without creating a synthetic workspace user. Set
SourceCreatedByUserId only when an import or restore preserves an existing
workspace user as the historical author. An unknown source user throws
MoltaroSdkException with the stable
MoltaroTagErrorCodes.SourceCreatorNotFound reason code; function runs persist
that value as FailureReasonCode and do not expose database constraint details.
Any other effective actor must identify a real workspace user or the operation
fails with MoltaroTagErrorCodes.ActorNotFound. Concurrent replacements for
the same resource are serialized, retries are idempotent, and tag changes emit
TagAdded, TagUpdated, or TagDeleted resource events in the same
transaction. Moltaro uses those events to retain function origin metadata,
update entity activity, and produce Board item audit/outbox side effects.
Scheduled automation is authorized through the platform’s owner/admin system
scope only while the runtime-owned tag mutation guard is active and the
function invocation carries platform-issued trusted-system provenance. Actor
strings from HTTP calls or persisted resource events cannot create that
provenance. The actor id is reserved from workspace-user creation and by a
database constraint, so supplying the string alone does not grant access.
Upgrades reject a legacy user row with that id until the conflicting user is
renamed or removed. OriginalUserId is attribution and does not grant access.
Direct-db tag events retain original-user and correlation metadata as a
non-function Moltaro.Tags source.
MoltaroTag.CreatedByUserId can be null when reading legacy rows created
before author attribution was required.
Entity and Board tag flags are deliberately separate. An EntityInstance owner
uses EntityDefinition.TagsEnabled; a BoardItem owner uses
Board.TagsEnabled. Ordinary access failures expose stable
MoltaroTagErrorCodes for disabled Entity tags, disabled Board tags, terminal
Board items that require historical reconciliation, access denial, unknown
owners, and unsupported owner kinds. The workspace Tags HTTP API returns the
same safe code and owner metadata in correlated Problem Details.
While a user has a current external-identity link, its identity-profile state
remains provider-owned; blocked state, role membership, and ExtendedData
remain mutable. DeleteUserAsync archives the current external-identity link
before soft-deleting the user so that the external subject can be linked again.
User, role, and responsibility-group DTOs expose ExtendedData for small
non-secret integration metadata. Keys should be source-namespaced, for example
ventcontrol.source-id. A null request value preserves existing metadata; an
empty dictionary clears it. The supported limit is 32 string entries, key
length 128, and value length 2048. moltaro.* and moltaro:* keys are
reserved. Extended data is read back by list/get calls, is not a permission
grant, and must not contain credentials, tokens, secrets, or full source
payloads.
The Board and Entitlement application services execute complete Moltaro
application orchestration in a separate scope as moltaro-system-automation,
while preserving original-user, function-run, and correlation metadata.
Command DTOs do not accept ActorUserId. Entitlement command DTOs require a
stable IdempotencyKey. AddBoardAutomationItemRequest requires a caller-owned
UUID OperationKey: preserve it for an exact retry, which returns the original
item without another write; changed-payload reuse conflicts. Other ordinary
Boards commands mirror Runtime API concurrency and repeat semantics without a
separate receipt. Rebuild existing consumers and pass the UUID as the first
AddBoardAutomationItemRequest constructor argument; the older constructor
without an operation key is no longer available. Historical
Board migration is the exception: ImportHistoricalItemsAsync uses durable,
case-sensitive per-Board source keys and
GetHistoricalImportAsync resolves unknown outcomes. Batches contain 1–500
items, commit per item, return ordered partial results, and require the latest
ExpectedRowVersion only when reconciling changed state. Exact replay returns
Unchanged without additional writes. Reconciliation updates the existing
Board Data record; its creation-time DataRecordId cannot be replaced. The
separate ReconcileHistoricalItemDataAsync operation repairs only
Subject/Description/Part values on terminal, non-removed, source-identified
historical items. It requires exactly one BoardItemId or case-sensitive
SourceKey locator plus ExpectedRowVersion; changed stale patches roll back,
while an exact replay is write-free and returns the current token. Omitting a
BoardHistoricalTextPatch preserves that text value and wrapping null clears
it. The operation preserves Board lifecycle, target, history, and timestamps;
ordinary closed-item UpdateDataAsync remains forbidden. A pre-existing Entity Definition Board
item uses the explicit GetHistoricalRecoveryStateAsync then
RecoverHistoricalItemAsync flow with the returned exact item id, row version,
and recommended adoption or post-removal mode. Self-contained, Entitlement,
and ambiguous multi-pass targets are rejected by recovery. These services are
supported only in Action, TriggerHandler, Command, Job, and HttpEndpoint
functions. Validation and BeforeSaveMutation receive the stable
moltaro.automation.executionPhase.unsupported error.
ReconcileHistoricalItemTagsAsync is the terminal-only tag counterpart. Its
1-500 item batch commits per owner and returns ordered partial results with
structured MoltaroRuntimeError code, field, target, and metadata. Every item
uses exactly one Board item id or historical source key and a row-version
guard. Add creates only missing names. ReplaceManagedSet changes only the
explicit ManagedTagNames set, preserving all manual/unrelated tags; an empty
desired tag list clears that managed set. Exact
replay is write-free; lifecycle, Board Data, completion/history, and item row
version are unchanged. Normal tag resource events carry the system actor and
function/run/correlation origin into Board audit and outbox processing.
Ordinary SDK tag mutation rejection throws MoltaroSdkException with the same
stable owner-specific ReasonCode/FailureCode, optional Field, and safe
Metadata used by the HTTP and automation contracts. Callers should branch on
the code and metadata, never on the diagnostic message.
The *.Runtime module services remain advanced trusted direct-DB APIs. They
are useful when package-style code deliberately owns all application side
effects, but they are not the default for triggers or jobs. See the
C# runtime recipes and
read ServiceKind/Preferred in the installation’s developer-surface
response.
Advanced direct-DB code that uses
ExecuteInTransactionIfNotExistsAsync receives one atomic boundary in both
hosting modes. The helper opens and owns a transaction when none exists and
acquires the shared dynamic-schema lock before invoking the action. When the
caller already owns a transaction, it acquires the same lock before creating a
nested savepoint;
an exception or a result rejected by shouldCommit restores both persisted
changes and the EF tracked state to that savepoint. The caller may then decide
whether to commit or roll back its outer transaction.
Managed secrets
Section titled “Managed secrets”Moltaro.Package.NET.Functions.ISecretService is available through constructor
injection in Job, Command, Action, TriggerHandler, Validation,
BeforeSaveMutation, and HttpEndpoint functions:
public sealed class SendToPartner(ISecretService secrets) : GlobalFunction{ public override Task<FunctionResult> OnRunAsync(CancellationToken cancellationToken) { var token = secrets.GetRequired("integrations.partner.api-token"); // Use token only for the outbound call. Do not log or return it. return Task.FromResult(FunctionResult.Ok("Partner call completed.")); }}GetRequired throws SecretUnavailableException; inspect Code against
SecretUnavailableCodes.Missing, Disabled, and Retired. TryGet returns
false with a null value for those states. Keys are case-insensitive, but null,
empty, whitespace-padded, or null-character-containing keys throw
ArgumentException.
The runtime captures one immutable snapshot before it creates the function. Rotation affects the next invocation, not repeated lookups in the current one. The service is a trusted-code boundary: any published trusted C# that knows a key can resolve it. Never place a value in arguments, results, DevLog, diagnostics, audit data, exports, or exception messages.
The canonical Managed secrets guide covers provisioning, lifecycle, audit, stable codes, safe token handling, agent rules, and a compile-checked Job example.
Function base classes
Section titled “Function base classes”ValidationFunction<TEntity>— rejects an entity save with validation issues. Bound to save operations (create, update, archive, restore, delete) with[EntitySaveBinding(...)]. See entity-scoped logic.MutationFunction<TEntity>— changes a record before create/update save, or runs after an operation when bound as a trigger with[EntityTrigger(...)]; covered on the same page.GlobalFunction/GlobalFunction<TArgs>— reusable logic without an entity binding. See global logic.CommandFunction<TResult>/CommandFunction<TArgs, TResult>— synchronous calls through the authenticated commands API. See commands and API functions.GlobalActionFunction<TInput>,EntityInstanceActionFunction<TEntity, TInput>,EntitySelectionActionFunction<TEntity, TInput>, andEntityCollectionActionFunction<TEntity, TInput>— user-triggered UI actions with typed input. See actions.HttpEndpointFunction/HttpEndpointFunction<TBody>— inbound HTTP endpoints served under/api/workspace/inbound/{endpointKey}, declared with[MoltaroHttpEndpoint]. See HTTP endpoints.
TEntity is the workspace entity class the function works with; argument,
input, and result types are JSON-serializable records you define.
For generated workspace entities, a Date and time field is a nullable
System.DateTimeOffset property backed by one PostgreSQL
timestamp(6) with time zone column. SDK SaveChanges normalizes values to
UTC and truncates precision beyond microseconds. The original numeric offset
is not a persisted attribute of the field.
Business Invariants
Section titled “Business Invariants”The SDK exposes the supported object-level Business Invariant contract under
Moltaro.Package.NET.BusinessInvariants. It defines
the generic BusinessInvariant<TEvaluationData> authoring base, the no-data
BusinessInvariant base, canonical resource and actor inputs, operation-key
constants, denial results, and the explicit IBusinessInvariantValidator
contract for function code.
Build analysis validates stable keys, typed targets, exact/category operation
scopes and exceptions, declared denial messages, parameter schema, supported
dependencies, and pure Grant code, including side effects reached through
source-defined helper methods. Package and Net Operation Project publication
produce equivalent deterministic descriptors and resolve embedded default text
without creating configuration.
Discovery alone does not enable an invariant; administrators own target
configuration. Enabled compatible targets shape current object-operation
decisions and automatically guard admitted Entity mutation owners. Function
execution is not intercepted automatically. Inject
IBusinessInvariantValidator, validate the exact existing resource and
function.{BusinessFunction.Key} with empty parameters, and stop before side
effects when the ordinary ValidationResult is invalid.
When an owning runtime adapter returns an invariant denial, its public API error
keeps the exact operation key in Target and the stable invariant source key in
Metadata.SourceKey.
Use the published constants instead of private operation strings. Board cycle
assignment and rollover both use BoardItemObjectOperationKeys.AssignCycle;
rollover validates every affected open item before moving any of them.
Identity and metadata
Section titled “Identity and metadata”Every function class carries [MoltaroFunction(id)]. The id is the durable
function id; an optional Key overrides the callable key (by default the id
is the key). Name and Description are shown to administrators and
configurators — set Description on any published function so its purpose is
clear in the Function Catalog. EnabledByDefault controls whether the
function starts enabled when it is first applied.
Publishing commands and enqueueable functions
Section titled “Publishing commands and enqueueable functions”[MoltaroCommand] publishes a command function at
POST /api/workspace/commands/{functionKey}. [MoltaroApiEnqueue] publishes
a Job-contract global function for asynchronous enqueue at
POST /api/workspace/functions/{functionKey}/enqueue; callers with access can
also observe and cancel the resulting queued job.
Both attributes are fail-closed: exactly one of PermissionKey or
AllowAnyAuthenticatedUser = true must be configured; a publication without
an access declaration is rejected at build and apply time, and anonymous
invocation is never supported. A permission key is a lowercase dotted
identifier such as acme.commands.recalculate — the segment after the last
dot becomes the catalog code, the leading segments the catalog group, so
administrators can assign it to roles.
MoltaroCommand.TimeoutSeconds optionally overrides the synchronous timeout,
from 1 through 86400 seconds. When unset, the runtime action timeout setting
applies — and the effective timeout never exceeds that installation setting.
MoltaroCommand.LicenseUsageMutation declares how the command can affect
licensed workspace usage. It defaults to
MoltaroLicenseUsageMutationKindEnum.Growth, which cannot run when the latest
background usage count already reaches a license limit. Use Neutral only when
the complete command cannot change licensed usage. Use ReducesUsage only when
it can exclusively release usage; this kind remains available at a limit.
Usage is counted in the background, by default every five minutes; the check
before a command only reads the latest count, takes no lock and never waits, so
growth committed after that count can exceed a limit until the next count.
Existing command publications are migrated as Growth. Adding a Neutral or
ReducesUsage declaration takes effect only after the Package or Net Operation
Project is rebuilt and applied, which synchronizes the new descriptor into the
active publication. Persisted publication rows are not an authoring surface.
The descriptor came with Package gateway contract 11 and Net Operation
Project gateway contract 8; the current SDK uses 16 and 13, and the
host runs only artifacts of its own contract. Source artifacts must be rebuilt
against this SDK; see
Rebuild after an SDK contract change.
MoltaroApiEnqueue.TimeoutSeconds optionally declares the worker timeout for
the queued job, also from 1 through 86400 seconds. Moltaro captures the value
when the API request creates the job, so every retry uses the same timeout even
if the publication or runtime defaults change. When unset, the runtime Job
timeout setting applies. API enqueue is always treated as licensed-usage
Growth because it persists a new Job; the command mutation declaration does
not apply to enqueue publications.
[MoltaroFunction("recalculate-credit", Name = "Recalculate credit", Description = "Recomputes the customer credit snapshot.")][MoltaroCommand(PermissionKey = "acme.commands.recalculate", LicenseUsageMutation = MoltaroLicenseUsageMutationKindEnum.Neutral, TimeoutSeconds = 120)]public sealed class RecalculateCredit : CommandFunction<RecalculateResult>{ public override async Task<CommandResult<RecalculateResult>> OnRunAsync( CancellationToken cancellationToken) { // ... business logic ... return CommandResult<RecalculateResult>.Ok(result); }}Failure results
Section titled “Failure results”A message passed to CommandResult.Failed(...) is returned to the caller as
user-facing text: keep it actionable and free of secrets or internal
diagnostics. Use CommandResult.ValidationFailed(...) to reject an
invocation with field-level validation issues instead of a generic failure.
For choosing between C# and declarative logic, project layout, and the build and apply workflow, continue with C# business logic.
Tasks from C#
Section titled “Tasks from C#”Tasks provides two supported paths. Prefer ITaskInstanceAutomationCommandService
and ITaskInstanceAutomationQueryService from
Moltaro.Package.NET.Tasks.Automation for application work. They share the Tasks
interface’s authorization, capacity, Parts, numbering, history, concurrency,
events and replay. The Net Operation Project includes the services in generated
dependency injection. See Tasks runtime automation
for asynchronous assignment, signals and agent rules.
The direct EF path uses public MoltaroOperationEntity classes in Moltaro.Package.NET.Tasks.Schema. GeneratedWorkspaceContext.TaskInstances and its configuration/history/interval DbSets are writable. Direct SaveChanges does not invoke operational commands: the author owns consistency, evidence and side effects. Database constraints remain active. Application commands commit in a separate scope and do not join an open authored EF transaction.
Static relationships include scalar foreign keys and typed EF navigations.
For example, query through task.TaskDefinition.Key, load
task.CreationRecord.CommandRecord with Include/ThenInclude, or filter
task.StateChanges in a LINQ projection. TaskInstance exposes optional
CreationRecord/CompletionRecord and collections of assignments, state changes,
commands and execution/interruption intervals. Queue work also exposes its
current TaskQueue and its QueueResidences; the open residence records when
the Task entered its current Queue. Navigation loading is explicit.
Reason and Outcome navigations refer to current configuration; saved historical
names, versions and comments remain on the evidence records.
InputDataBinding and WorkingDataBinding refer to ownership metadata. The actual Input/Working records have Definition-specific generated Entity types and are queried through their own DbSets. These relationships are also available in the generated workspace context.
using Moltaro.Package.NET.Functions;using Moltaro.Package.NET.Tasks.Automation;using Moltaro.Package.NET.Tasks.Instances;
public sealed record CreateTaskArgs(string OperationId, string DefinitionId, string WorkerId);
[MoltaroFunction("tasks.createDirect", Key = "tasks.createDirect", Name = "Create Direct work")]public sealed class CreateDirectWork(ITaskInstanceAutomationCommandService commands) : GlobalFunction<CreateTaskArgs>{ public override async Task<FunctionResult> OnRunAsync(CreateTaskArgs args, CancellationToken token) { var result = await commands.CreateDirectAsync(new CreateDirectTaskRequest( args.OperationId, args.DefinitionId, args.WorkerId, Subject: "Scheduled work"), token); result.ThrowIfFailed(); return FunctionResult.OkData(new { result.Data!.Replayed, TaskId = result.Data.Result?.Id }); }}Choose an active Direct Definition and a real admitted Worker. Creation does not Start execution. For Direct work, services also support assignment, Start/Stop, Pause/Resume, Return to Idle, Block/Resolve, timed Postpone with early return, and Complete/Cancel; Queue work is described below. Read the current RowVersion before a new command. Preserve OperationId for exact retries after lost responses; a new intention needs a new key. Success may omit the result if View is absent.
CreateDirectTaskRequest also accepts Description, DueAtUtc, Context,
Input and Working field values. The Definition’s due-date mode, Context
admission, the caller’s own Context access and the Working edit rule for Idle
decide whether they are accepted; Entity field errors name Input.<key> or
Working.<key>. Omit a Part key to receive the field’s configured default;
send an explicit null to leave it empty. The two payloads are different
intentions under one OperationId, and a Working section that contains only
null values still needs the Working grant. Existing work is changed through
property and Part commands:
| Command | Request | Definition grant |
|---|---|---|
EditDetailsAsync | TaskInstanceDetailsRequest (Subject, Description) | EditDetails |
ChangeDueDateAsync | TaskInstanceDueDateRequest (UTC instant or null) | ChangeDueDate and due-date mode |
ChangePriorityAsync | TaskInstancePriorityRequest (Priority, SubPriority) | ChangePriority |
ChangeContextAsync | TaskInstanceContextRequest (admitted identity or null) | ChangeContext and target access |
EditInputDataAsync | TaskInstanceDataRequest (Task and Part RowVersion, field patch) | EditInputData |
EditWorkingDataAsync | TaskInstanceDataRequest (Task and Part RowVersion, field patch) | EditWorkingData |
Every request carries the Task RowVersion; a Part patch also carries the
Part’s RowVersion from GetDataAsync or GetEditOptionsAsync. Omitted Part
keys stay unchanged and an explicit null clears a value. A Part request sent
with the current Task and Part versions in which every value equals the stored
value is a no-op: no new Task or Part version, evidence or event. The exception
is a sent field that triggers a before-save change of the Part: the request
then goes through the Part’s record update, which creates a new Part version
even when no value changes. The Task gets a new version, a history entry and an
event only when a Part value actually changed, and then records only those
keys; otherwise keep the Task RowVersion and re-read the Part version before
the next Part command. A
request with any change creates a new Part version and records only the changed
keys. Both versions are part of the command identity, so a
retry must repeat them exactly. Queries GetDataAsync (one Part’s readable
values and version; a Kind outside Input/Working is a validation failure),
GetInstructionsAsync (current Definition instructions and file references,
read through Task authority) and GetEditOptionsAsync (the property/Part
actions permitted now) complete the surface. ListAsync accepts an optional
due window DueFromUtc/DueToUtc: the lower bound is inclusive, the upper
bound exclusive, so a calendar day ends at the next local midnight. History
entries of these commands carry Changes: one entry per changed key with its
ValueKind and typed previous and new values (Text, Integer, AtUtc, or
Context with a label the caller may see); a Part key carries only its label,
never a value. Direct EF readers find the same facts as
TaskPropertyChangeValue rows and decode them with
TaskPropertyChangeValueCodec. A Subject is one line: a line break or any
other control character is rejected.
Before creating a task, GetCreateFormAsync(definitionId) returns its Subject
and due-date modes, admitted Context definitions and both complete Entity forms.
The forms are detached JsonElement values using the HTTP Entity form schema,
including field options, defaults and validation rules. The typed
WorkingCapability describes the default participants. Call
GetCreateWorkingCapabilityAsync after choosing different Owner or Responsible
participants; the create command checks the grant again.
GetCreateContextCandidatesAsync searches readable targets of an admitted
Context definition before creation. GetContextCandidatesAsync does the same
under an existing task’s Context edit grant. Both queries take a bounded Take
and a Skip that is a multiple of Take; their result includes the exact total.
GetEditOptionsAsync includes SubjectMode, due-date mode and the complete Context
policy. GetCommandOptionsAsync returns the lifecycle actions available now and,
for Queue work, take, take-and-start and assign; each action has an
enabled flag and a reason code. Property and Part actions belong to
GetEditOptionsAsync.
Priority and SubPriority remain long in C#. Their JSON representations are
decimal strings, preserving every signed 64-bit value; numeric input is also
accepted. Priority sorting uses Priority, SubPriority and task identity in the
requested direction. Instruction metadata is available through the SDK; file
bytes use the authenticated Task instruction-file HTTP endpoint, which checks
current Task access, Definition binding and file readiness.
using System.Text.Json;using Moltaro.Package.NET.Functions;using Moltaro.Package.NET.Tasks.Automation;using Moltaro.Package.NET.Tasks.Instances;
public sealed record RecordMeasurementArgs(string OperationId, string TaskId, long MeasuredQuantity);
[MoltaroFunction("tasks.recordMeasurement", Key = "tasks.recordMeasurement", Name = "Record a measurement")]public sealed class RecordMeasurement(ITaskInstanceAutomationCommandService commands, ITaskInstanceAutomationQueryService queries) : GlobalFunction<RecordMeasurementArgs>{ public override async Task<FunctionResult> OnRunAsync(RecordMeasurementArgs args, CancellationToken token) { var options = await queries.GetEditOptionsAsync(args.TaskId, token); options.ThrowIfFailed(); var fields = new Dictionary<string, JsonElement> { ["MeasuredQuantity"] = JsonSerializer.SerializeToElement(args.MeasuredQuantity, new JsonSerializerOptions()) }; var result = await commands.EditWorkingDataAsync(new TaskInstanceDataRequest( args.OperationId, args.TaskId, options.Data!.RowVersion, options.Data.WorkingRowVersion, fields), token); result.ThrowIfFailed(); return FunctionResult.OkData(new { result.Data!.Replayed, RowVersion = result.Data.Result?.RowVersion }); }}The Net Operation Project supports instance services in Action, Command, HttpEndpoint, TriggerHandler and Job functions. CRON and triggers carry host-authenticated system provenance, without a fabricated user/Worker. Human invocations retain ordinary permissions; an actor string cannot grant authority. Validation and BeforeSaveMutation cannot call application services.
Queue work
Section titled “Queue work”Queue work uses the same services, RowVersion and OperationId rules.
CreateQueueAsync takes a CreateQueueTaskRequest with OperationId,
DefinitionId, QueueId, an optional managing Responsible, Owner and Subject,
and the same Description, due date, Context and Part values as Direct creation.
The Queue must be Active and admit the active Queue-mode Definition; the creator
does not need to belong to the Queue’s Worker pool. The new Task is Idle, has no
Executor and enters the Queue immediately. Depending on the Definition, the
managing Responsible is not used, optional or required at creation; this person
manages the work but never performs it. Sending an active Queue-mode Definition
to CreateDirectAsync fails with tasks.runtime.queue.required and a QueueId
field error.
| Member | Purpose |
|---|---|
ListQueuesAsync | Queues the caller can discover for operational work, with the number of unfinished Tasks the caller can see, CanAcquire and CanDispatch. It is not the configuration catalog and grants no Task access. |
ListAsync with QueueId | Scope = Visible (the default) lists the caller’s visible Tasks of that Queue. Scope = Available lists the caller’s current acquisition candidates: every eligible Task under Any, only the first or last one under First or Last. Candidates are fixed before search, filters, count and paging. Sort Queue follows the Queue’s configured order; QueueEntry sorts by the time the Task entered the Queue. |
TakeAsync | The calling Worker takes the exact Task named by TaskInstanceTakeRequest (QueueId, TaskId, RowVersion) and becomes its Executor. The Task stays Idle. |
TakeAndStartAsync | Takes and starts the Task in one transaction; Start permissions and execution limits are checked together with the assignment. |
AssignExecutorAsync | A dispatcher of the Task’s Queue or a Task administrator of its Definition assigns, changes or clears (ClearAssignment) the Executor of any unfinished Task with TaskInstanceQueueAssignmentRequest. Running work stops and a pause or postponement ends, so the Task arrives Idle; a block stays; nothing starts. Changing or clearing an assigned Executor requires Reason (since 1.22.0, also for Idle work). |
ReturnToQueueAsync | The Executor gives the Task back where the Queue’s return policy is AllowedWithReason; Comment is the required reason. The Task waits Idle and unassigned at its place in the Queue; a block stays. Under the default NotAllowed policy the call fails with tasks.runtime.queue.returnNotAllowed. |
AssignResponsibleAsync, AssignOwnerAsync | Change the managing Responsible or Owner of Queue work; the Executor is unchanged. |
PostponeAsync | Postpones Idle, InProgress or Paused work of an assigned performer for the Definition’s fixed delay with a Postponed reason. It keeps the Executor and the Queue position and uses one of the Definition’s lifetime attempts; Worker Profile and Queue postponed limits apply. PostponedUntilUtc on the snapshot is the return time, and GetCommandOptionsAsync returns the Postponement allowance. The platform returns the Task to Idle when the time comes. |
ReturnFromPostponementAsync, ReturnFromPostponementAndStartAsync | End a postponement early, to Idle or straight into work under the ordinary Start checks. Anyone but the performer, automation included, must give a Comment as the reason; ReasonRequired on the action in GetCommandOptionsAsync says so in advance. |
GetQueueWorkloadAsync | The current load of the Queue’s pool Workers that the caller can see, across all their work: counts against Worker Profile limits and Overloaded, never the Tasks behind them. Only the Queue’s dispatchers and Task administrators of a Definition it admits receive it, as CanViewWorkload in ListQueuesAsync shows; anyone else gets a not-found failure. |
Take and Take-and-Start are self-service only. The caller must be a Worker in the
Queue’s pool with free assigned-Task capacity; a Worker outside the pool who can
see the Task gets a Forbidden failure, and Jobs or triggers, which run without a
Worker, cannot take work. If the Task is no longer the current candidate or its
RowVersion changed, the command fails with a conflict and never takes a
different Task. A dispatcher can
exceed a recipient’s assigned-Task limit only with CapacityOverride, a
non-empty Reason and a Worker Profile that allows dispatcher exceptions, and
never for their own Worker; a Task administrator who is not a dispatcher cannot
use one. The exception is recorded in history as
TaskInstanceCapacityException. Since contract 1.22.0 a call that changes or
clears an assigned Executor without a Reason fails with
tasks.runtime.executor.reasonRequired on field Reason, also for Idle work that
earlier versions accepted; give the reason when you reassign or unassign. A
Worker who returned work may no longer see it, so the result of
ReturnToQueueAsync can be null; repeating the call with the same OperationId
still replays it.
An Active Queue accepts new Tasks. A Draining Queue accepts no new Tasks but
still serves the Tasks it holds, and an Inactive Queue serves none; a Queue
cannot be made Inactive while it still has unfinished Tasks. Queue work supports
Start, Stop, Complete and Cancel under the Definition’s permissions, with the
Executor as performer. Snapshots add Mode, TaskQueueId,
ManagingResponsibleWorkerId, ExecutorWorkerId, QueueEnteredAtUtc and
QueueManualRank; ResponsibleWorkerId is the Direct Responsible or the Queue
managing Responsible. The Executor pauses, resumes, returns to Idle, blocks and
postpones Queue work like a Direct Responsible. Under the default permissions
the Queue’s dispatchers and managing Responsible can also block the work and
resolve any block; the Executor resolves only a block they reported.
Not available in this version: Auto-assignment and Assignment on request, reassigning executing, paused, blocked or postponed Queue work, Queue return, handoff and transfer, and comments and files on Queue work.
Tasks configuration automation describes the separate Definition, Queue and Worker Profile services and their permissions.