Skip to content

Grain Definition

Complete guide to the grain computation expression.

Complete guide to the grain { } computation expression.

  • Every keyword in the grain { } CE
  • How to define handlers, lifecycle hooks, reminders, and timers
  • Placement strategies, reentrancy, and stateless workers
  • Multiple named states, implicit stream subscriptions, and more

The grain { } CE builds a GrainDefinition<'State, 'Message> — an immutable record that fully describes a grain’s behavior. The Orleans.FSharp.CodeGen package reads this definition at build time and generates the corresponding C# grain class with all the correct Orleans attributes.

open Orleans.FSharp
let myGrain = grain {
defaultState initialValue
handle handlerFunction
persist "StorageProviderName"
}

Every grain definition requires at minimum:

  1. A defaultState — the initial state value
  2. At least one handler from the table below
VariantSignatureUse when
handle'S -> 'M -> Task<'S * obj>Full control, manual box
handleState'S -> 'M -> Task<'S>Caller only needs state
handleTyped'S -> 'M -> Task<'S * 'R>Typed result, no box
handleWithContextGrainContext -> 'S -> 'M -> Task<'S * obj>Need grain-to-grain calls or DI
handleStateWithContextGrainContext -> 'S -> 'M -> Task<'S>Context + state-only return
handleTypedWithContextGrainContext -> 'S -> 'M -> Task<'S * 'R>Context + typed result
handleCancellable'S -> 'M -> CancellationToken -> Task<'S * obj>Long-running ops, manual box
handleStateCancellable'S -> 'M -> CancellationToken -> Task<'S>Long-running ops, state-only
handleTypedCancellable'S -> 'M -> CancellationToken -> Task<'S * 'R>Long-running ops, typed result
handleWithContextCancellableGrainContext -> 'S -> 'M -> CancellationToken -> Task<'S * obj>Context + cancellation
handleStateWithContextCancellableGrainContext -> 'S -> 'M -> CancellationToken -> Task<'S>Context + cancellation, state-only
handleTypedWithContextCancellableGrainContext -> 'S -> 'M -> CancellationToken -> Task<'S * 'R>Context + cancellation, typed result

Aliases: handleWithServices = handleWithContext, handleStateWithServices = handleStateWithContext, handleWithServicesCancellable = handleWithContextCancellable, handleStateWithServicesCancellable = handleStateWithContextCancellable, handleTypedWithServicesCancellable = handleTypedWithContextCancellable.


Sets the initial state of the grain when first activated. Required for every grain.

let myGrain = grain {
defaultState 0
handle (fun state msg -> task { return state + 1, box(state + 1) })
}

For DU state:

let myGrain = grain {
defaultState Zero
handle (fun state msg -> task { return Count 1, box 1 })
}

Registers the message handler. Takes the current state and a message, returns a Task<'State * obj> (new state and a boxed result).

grain {
defaultState 0
handle (fun state msg ->
task {
match msg with
| Add n -> return state + n, box(state + n)
| Get -> return state, box state
})
}

A simpler variant of handle for grains where the caller only needs the updated state. The handler returns Task<'State> — no need to box a result separately.

grain {
defaultState { Count = 0 }
handleState (fun state msg ->
task {
match msg with
| Increment -> return { Count = state.Count + 1 }
| Decrement -> return { Count = state.Count - 1 }
| GetValue -> return state
})
}

The result returned to the caller is the new state (boxed internally).

Like handle, but accepts a strongly-typed result instead of obj. No manual box call needed.

grain {
defaultState { Count = 0 }
handleTyped (fun state msg ->
task {
match msg with
| Increment ->
let ns = { Count = state.Count + 1 }
return ns, ns.Count // 'Result = int — no box needed
| GetValue ->
return state, state.Count
})
}

The 'Result type is inferred from the handler return type. The framework boxes it internally before sending to the caller.


Like handle, but the handler receives a GrainContext as the first argument. Use this when you need to call other grains or resolve DI services from within the handler.

grain {
defaultState Map.empty
handleWithContext (fun ctx state msg ->
task {
match msg with
| Aggregate key ->
let otherGrain =
GrainContext.getGrainByString<IOtherGrain> ctx key
let! value = GrainRef.invoke otherGrain (fun g -> g.GetValue())
return state |> Map.add key value, box value
})
}

The GrainContext provides:

MemberDescription
GrainContext.getService<'T>Resolve a DI service
GrainContext.getState<'T> nameGet a named additional persistent state
GrainContext.getGrainByString<'T> keyGet a grain ref by string key
GrainContext.getGrainByGuid<'T> keyGet a grain ref by GUID key
GrainContext.getGrainByInt64<'T> keyGet a grain ref by int64 key
GrainContext.getGrainByGuidCompound<'T> guid extCompound GUID+string key
GrainContext.getGrainByIntCompound<'T> key extCompound int64+string key
GrainContext.deactivateOnIdleRequest deactivation when idle
GrainContext.delayDeactivation spanDelay deactivation
GrainContext.grainIdGet the GrainId
GrainContext.primaryKeyStringGet the string primary key
GrainContext.primaryKeyGuidGet the Guid primary key
GrainContext.primaryKeyInt64Get the int64 primary key

Like handleWithContext, but returns only the new state — no need to manually box a result. Use this when the handler needs GrainContext but the caller only needs the updated state.

grain {
defaultState { Score = 0 }
handleStateWithContext (fun ctx state msg ->
task {
match msg with
| AddPoints n ->
return { Score = state.Score + n }
| SubtractPoints n ->
return { Score = state.Score - n }
| NotifyAchievement achievementId ->
let notifier = GrainContext.getService<IAchievementNotifier> ctx
do! notifier.NotifyAsync(achievementId)
return state
})
}

Pair with FSharpGrain.send to receive the updated state back.

Like handleWithContext, but returns a strongly-typed result — no manual box call needed. Combines context access with the clean handleTyped return style.

grain {
defaultState { Balance = 0m }
handleTypedWithContext (fun ctx state msg ->
task {
match msg with
| Deposit amount ->
let ns = { Balance = state.Balance + amount }
return ns, ns.Balance // 'Result = decimal
| GetBalance ->
return state, state.Balance
})
}

Pair with FSharpGrain.ask<'S,'C,decimal> to receive the typed result.

Alias for handleWithContext that emphasizes DI access. Identical behavior.

grain {
defaultState []
handleWithServices (fun ctx state msg ->
task {
let logger = GrainContext.getService<ILogger<_>> ctx
Log.logInfo logger "Processing {Command}" [| box msg |]
return state, box ()
})
}

Like handle, but the handler receives a CancellationToken for cooperative cancellation of long-running operations.

grain {
defaultState ""
handleCancellable (fun state msg ct ->
task {
let! result = longRunningOperation ct
return result, box result
})
}

Like handleState, but the handler receives a CancellationToken. Returns only the updated state — no manual box needed.

grain {
defaultState { Items = [] }
handleStateCancellable (fun state msg ct ->
task {
match msg with
| FetchAndAppend url ->
let! item = fetchWithTimeout url ct
return { Items = item :: state.Items }
| Clear ->
return { Items = [] }
})
}

The CancellationToken comes from the Orleans runtime and allows long-running fetch/IO operations to be cancelled cleanly when the silo shuts down.

Like handleTyped, but the handler also receives a CancellationToken. Returns a strongly-typed result — no box needed.

grain {
defaultState { Results = [] }
handleTypedCancellable (fun state msg ct ->
task {
match msg with
| ComputeSum inputs ->
let sum = List.sum inputs
return { Results = sum :: state.Results }, sum // 'Result = int
| GetLatest ->
let latest = state.Results |> List.tryHead |> Option.defaultValue 0
return state, latest
})
}

Pair with FSharpGrain.ask<'S,'C,int> to receive the typed result.

Combines GrainContext and CancellationToken.

grain {
defaultState 0
handleWithContextCancellable (fun ctx state msg ct ->
task {
let httpClient = GrainContext.getService<HttpClient> ctx
let! response = httpClient.GetAsync("https://api.example.com", ct)
return state + 1, box response.StatusCode
})
}

Alias for handleWithContextCancellable.

Combines GrainContext, CancellationToken, and state-only return — no manual box needed. The maximum set of inputs with the simplest return.

grain {
defaultState { Items = [] }
handleStateWithContextCancellable (fun ctx state msg ct ->
task {
match msg with
| FetchAndStore url ->
let http = GrainContext.getService<HttpClient> ctx
let! body = http.GetStringAsync(url, ct)
return { Items = body :: state.Items }
| Clear ->
return { Items = [] }
})
}

Alias for handleStateWithContextCancellable.

The full combination: GrainContext, CancellationToken, and a typed result — no box needed.

grain {
defaultState { Total = 0 }
handleTypedWithContextCancellable (fun ctx state msg ct ->
task {
match msg with
| FetchAndAdd url ->
let http = GrainContext.getService<HttpClient> ctx
let! n = http.GetStringAsync(url, ct) |> Task.map int
return { Total = state.Total + n }, state.Total + n // 'Result = int
| GetTotal ->
return state, state.Total
})
}

Pair with FSharpGrain.ask<'S,'C,int> to receive the typed result.

Alias for handleTypedWithContextCancellable.


Names the Orleans storage provider for state persistence. The provider must be registered in the silo configuration.

grain {
defaultState Zero
handle (fun state msg -> task { return Count 1, box 1 })
persist "Default"
}

Without persist, the grain state is in-memory only and is lost on deactivation.

Declares a named secondary persistent state that can be accessed via GrainContext.getState. Use this when a grain needs multiple independently persisted state values.

grain {
defaultState { Balance = 0m }
additionalState "AuditLog" "AuditStore" ([] : AuditEntry list)
handleWithContext (fun ctx state msg ->
task {
let auditState =
GrainContext.getState<AuditEntry list> ctx "AuditLog"
let currentAudit = auditState.State
// ... process and update both states
return newState, box result
})
persist "Default"
}

Parameters: additionalState name storageName defaultValue


Runs when the grain is activated. Receives the current state and returns a potentially modified state.

grain {
defaultState { Cache = Map.empty; LastRefresh = DateTime.MinValue }
handle myHandler
persist "Default"
onActivate (fun state ->
task {
printfn "Grain activated with state: %A" state
return { state with LastRefresh = DateTime.UtcNow }
})
}

Runs when the grain is being deactivated. Receives the current state for cleanup.

grain {
defaultState { ConnectionId = None }
handle myHandler
persist "Default"
onDeactivate (fun state ->
task {
match state.ConnectionId with
| Some id -> printfn "Closing connection %s" id
| None -> ()
})
}

Hooks into Orleans grain lifecycle stages for fine-grained control. Standard stages:

StageValueDescription
GrainLifecycleStage.First2000First stage after creation
GrainLifecycleStage.SetupState4000State setup
GrainLifecycleStage.Activate6000Activation
GrainLifecycleStage.Lastint.MaxValueFinal stage
grain {
defaultState 0
handle myHandler
onLifecycleStage 2000 (fun ct ->
task {
printfn "Early lifecycle hook firing"
})
onLifecycleStage 6000 (fun ct ->
task {
printfn "Activation-stage hook firing"
})
}

Multiple hooks at the same stage are executed in registration order.


Registers a named reminder handler. Reminders are persistent periodic triggers that survive grain deactivation and silo restarts. The silo must have a reminder service configured.

grain {
defaultState { CheckCount = 0 }
handle myHandler
persist "Default"
onReminder "HealthCheck" (fun state reminderName tickStatus ->
task {
printfn "Reminder %s fired at %A" reminderName tickStatus.CurrentTickTime
return { state with CheckCount = state.CheckCount + 1 }
})
}

To register the reminder at runtime, use the Reminder module:

Reminder.register grain "HealthCheck" (TimeSpan.FromMinutes 1.) (TimeSpan.FromMinutes 5.)

Registers a declarative timer that is automatically started on grain activation and stopped on deactivation. Timers are in-memory only — they do not survive deactivation.

grain {
defaultState { HeartbeatCount = 0 }
handle myHandler
onTimer
"Heartbeat"
(TimeSpan.FromSeconds 10.) // dueTime: first fire after 10s
(TimeSpan.FromSeconds 30.) // period: then every 30s
(fun state ->
task {
return { state with HeartbeatCount = state.HeartbeatCount + 1 }
})
}

Marks the grain as reentrant, allowing concurrent message processing. By default, Orleans grains process one message at a time.

grain {
defaultState Map.empty
handle myReadHeavyHandler
reentrant
}

Marks a specific method as always interleaved, even on non-reentrant grains. Use for methods that are safe to run concurrently.

grain {
defaultState { Data = Map.empty }
handle myHandler
interleave "GetStatus"
interleave "GetVersion"
}

Marks a method as read-only. Read-only methods are interleaved for concurrent reads on non-reentrant grains.

grain {
defaultState { Items = [] }
handle myHandler
readOnly "GetItems"
readOnly "GetCount"
}

Sets a custom predicate method for reentrancy decisions. The named static method receives an InvokeMethodRequest and returns bool.

grain {
defaultState Map.empty
handle myHandler
mayInterleave "ShouldInterleave"
}

Marks the grain as a stateless worker, allowing multiple activations per silo for load balancing. Stateless workers cannot use persistent state.

grain {
defaultState ()
handle (fun _ msg ->
task {
let result = processMessage msg
return (), box result
})
statelessWorker
}

Sets the maximum number of local worker activations per silo. Defaults to the number of CPU cores.

grain {
defaultState ()
handle myHandler
statelessWorker
maxActivations 4
}

Control which silo activates a grain:

// Prefer the silo where the call originated
grain { ... ; preferLocalPlacement }
// Random silo
grain { ... ; randomPlacement }
// Consistent hash of grain ID
grain { ... ; hashBasedPlacement }
// Silo with fewest activations
grain { ... ; activationCountPlacement }
// Resource-aware (CPU, memory)
grain { ... ; resourceOptimizedPlacement }
// Target silos with a specific role
grain { ... ; siloRolePlacement "worker" }
// Custom strategy type
grain { ... ; customPlacement typeof<MyPlacementStrategy> }

Auto-subscribes the grain to a stream namespace. The handler receives the current state and a stream event (boxed), and returns the new state.

grain {
defaultState { Events = [] }
handle myHandler
persist "Default"
implicitStreamSubscription "OrderEvents" (fun state event ->
task {
let orderEvent = event :?> OrderEvent
return { state with Events = orderEvent :: state.Events }
})
}

Marks a method as fire-and-forget. The caller does not wait for the grain to finish processing.

grain {
defaultState ()
handle myHandler
oneWay "LogEvent"
}

Sets a custom grain type name (maps to [GrainType("name")] in CodeGen).

grain {
defaultState 0
handle myHandler
grainType "my-counter-v2"
}

Sets the per-grain idle timeout before deactivation.

grain {
defaultState Map.empty
handle myHandler
deactivationTimeout (TimeSpan.FromMinutes 30.)
}

Here is a grain that uses many features together:

open System
open System.Threading.Tasks
open Orleans.FSharp
[<GenerateSerializer>]
type ChatState =
| [<Id(0u)>] Empty
| [<Id(1u)>] Active of messages: string list * participants: Set<string>
[<GenerateSerializer>]
type ChatCommand =
| [<Id(0u)>] Join of user: string
| [<Id(1u)>] Leave of user: string
| [<Id(2u)>] Send of user: string * text: string
| [<Id(3u)>] GetHistory
| [<Id(4u)>] GetParticipants
let chatRoom =
grain {
defaultState Empty
handleWithContext (fun ctx state cmd ->
task {
match state, cmd with
| Empty, Join user ->
let newState = Active([], Set.singleton user)
return newState, box true
| Active(msgs, users), Join user ->
return Active(msgs, users |> Set.add user), box true
| Active(msgs, users), Leave user ->
let remaining = users |> Set.remove user
if Set.isEmpty remaining then
return Empty, box true
else
return Active(msgs, remaining), box true
| Active(msgs, users), Send(user, text) ->
let entry = $"[{DateTime.UtcNow:HH:mm}] {user}: {text}"
return Active(entry :: msgs, users), box entry
| _, GetHistory ->
let msgs = match state with Active(m, _) -> m | _ -> []
return state, box msgs
| _, GetParticipants ->
let users = match state with Active(_, u) -> u | _ -> Set.empty
return state, box users
| _ -> return state, box false
})
persist "Default"
reentrant
readOnly "GetHistory"
readOnly "GetParticipants"
deactivationTimeout (TimeSpan.FromHours 1.)
onTimer "Cleanup" (TimeSpan.FromMinutes 5.) (TimeSpan.FromMinutes 5.)
(fun state ->
task {
match state with
| Active(msgs, users) when msgs.Length > 1000 ->
return Active(msgs |> List.take 500, users)
| _ -> return state
})
}
  • Silo Configuration — configure storage, clustering, and streaming for your grains
  • Streaming — publish and subscribe to events
  • Testing — test your grain definitions with FsCheck and TestHarness
  • Advanced — behavior pattern: state machines with Behavior.run / Behavior.runWithContext