docs/modules/module-state.mdx
Distributed key-value state storage with scope-based organization and reactive triggers that fire on any state change.
modules::state::StateModule
graph LR
W1[Worker] -->|state::set| Engine[Engine]
Engine -->|Persist| Adapter[State Adapter]
Adapter -->|old + new value| Engine
Engine -->|Result| W1
Engine -->|Match| Triggers[Trigger Registry]
Triggers -.->|Fire Handler| W2[Worker]
State is server-side key-value storage with trigger-based reactivity. Unlike streams, state does not push updates to WebSocket clients — it fires triggers that workers handle server-side.
- class: modules::state::StateModule
config:
adapter:
class: modules::state::adapters::KvStore
config:
store_method: file_based
file_path: ./data/state_store
save_interval_ms: 5000
Built-in key-value store. Supports both in-memory and file-based persistence.
class: modules::state::adapters::KvStore
config:
store_method: file_based
file_path: ./data/state_store
save_interval_ms: 5000
Uses Redis as the state backend.
class: modules::state::adapters::RedisAdapter
config:
redis_url: ${REDIS_URL:redis://localhost:6379}
Forwards state operations to a remote III Engine instance via the Bridge Client.
class: modules::state::adapters::Bridge
| Operation | Shape | Description |
|-----------|-------|-------------|
| `set` | `{ "set": value }` | Replace the current value entirely. |
| `merge` | `{ "merge": object }` | Shallow-merge an object into the current value. |
| `increment` | `{ "increment": { "field": string, "by": number } }` | Add `by` to a numeric field. |
| `decrement` | `{ "decrement": { "field": string, "by": number } }` | Subtract `by` from a numeric field. |
| `remove` | `{ "remove": field }` | Remove a field from the current object. |
</ResponseField>
</Accordion>
<Accordion title="Returns">
<ResponseField name="old_value" type="any">
The value before the operations were applied, or `null` if the key did not exist.
</ResponseField>
<ResponseField name="new_value" type="any">
The value after all operations were applied.
</ResponseField>
</Accordion>
This module adds a new Trigger Type: state.
When a state value is created, updated, or deleted, all registered state triggers are evaluated and fired if they match.
When the trigger fires, the handler receives a state event object:
<ResponseField name="type" type="string"> Always `"state"`. </ResponseField> <ResponseField name="event_type" type="string"> The kind of change: `"state:created"`, `"state:updated"`, or `"state:deleted"`. </ResponseField> <ResponseField name="scope" type="string"> The scope where the change occurred. </ResponseField> <ResponseField name="key" type="string"> The key that changed. </ResponseField> <ResponseField name="old_value" type="any"> The previous value before the change, or `null` for newly created keys. </ResponseField> <ResponseField name="new_value" type="any"> The new value after the change. `null` for deleted keys. </ResponseField>iii.registerTrigger({ type: 'state', function_id: fn.id, config: { scope: 'users', key: 'profile' }, })
</Tab>
<Tab title="Python">
```python
def on_user_updated(event):
print('State changed:', event['event_type'], event['key'])
print('Previous:', event.get('old_value'))
print('Current:', event.get('new_value'))
return {}
iii.register_function({'id': 'state::onUserUpdated'}, on_user_updated)
iii.register_trigger({'type': 'state', 'function_id': 'state::onUserUpdated', 'config': {'scope': 'users', 'key': 'profile'}})
iii.register_function( RegisterFunctionMessage { id: "state::onUserUpdated".into(), description: None, request_format: None, response_format: None, metadata: None, invocation: None }, |event| async move { println!("State changed: {} {}", event["event_type"], event["key"]); println!("Previous: {:?}", event.get("old_value")); println!("Current: {:?}", event.get("new_value")); Ok(json!({})) }, );
iii.register_trigger(RegisterTriggerInput { trigger_type: "state".into(), function_id: "state::onUserUpdated".into(), config: json!({ "scope": "users", "key": "profile" }), })?;
</Tab>
</Tabs>
### Usage Example: User Profile with Reactive Sync
Store user profiles in state and react when they change:
<Tabs>
<Tab title="Node / TypeScript">
```typescript
await iii.trigger({
function_id: 'state::set',
payload: {
scope: 'users',
key: 'user-123',
value: { name: 'Alice', email: '[email protected]', preferences: { theme: 'dark' } },
},
action: TriggerAction.Void(),
})
const profile = await iii.trigger({
function_id: 'state::get',
payload: { scope: 'users', key: 'user-123' },
})
await iii.trigger({
function_id: 'state::set',
payload: {
scope: 'users',
key: 'user-123',
value: { name: 'Alice', email: '[email protected]', preferences: { theme: 'light' } },
},
action: TriggerAction.Void(),
})
const allUsers = await iii.trigger({
function_id: 'state::list',
payload: { scope: 'users' },
})
const scopes = await iii.trigger({
function_id: 'state::list_groups',
payload: {},
})
profile = iii.trigger({ 'function_id': 'state::get', 'payload': {'scope': 'users', 'key': 'user-123'}, })
iii.trigger({ 'function_id': 'state::set', 'payload': { 'scope': 'users', 'key': 'user-123', 'value': {'name': 'Alice', 'email': '[email protected]', 'preferences': {'theme': 'light'}}, }, 'action': {'type': 'void'}, })
all_users = iii.trigger({ 'function_id': 'state::list', 'payload': {'scope': 'users'}, }) scopes = iii.trigger({ 'function_id': 'state::list_groups', 'payload': {}, })
</Tab>
<Tab title="Rust">
```rust
use iii_sdk::{TriggerRequest, TriggerAction};
use serde_json::json;
iii.trigger(TriggerRequest {
function_id: "state::set".into(),
payload: json!({
"scope": "users",
"key": "user-123",
"value": { "name": "Alice", "email": "[email protected]", "preferences": { "theme": "dark" } }
}),
action: Some(TriggerAction::Void),
timeout_ms: None,
}).await?;
let profile = iii.trigger(TriggerRequest {
function_id: "state::get".into(),
payload: json!({ "scope": "users", "key": "user-123" }),
action: None,
timeout_ms: None,
}).await?;
iii.trigger(TriggerRequest {
function_id: "state::set".into(),
payload: json!({
"scope": "users",
"key": "user-123",
"value": { "name": "Alice", "email": "[email protected]", "preferences": { "theme": "light" } }
}),
action: Some(TriggerAction::Void),
timeout_ms: None,
}).await?;
let all_users = iii.trigger(TriggerRequest {
function_id: "state::list".into(),
payload: json!({ "scope": "users" }),
action: None,
timeout_ms: None,
}).await?;
let scopes = iii.trigger(TriggerRequest {
function_id: "state::list_groups".into(),
payload: json!({}),
action: None,
timeout_ms: None,
}).await?;
Only process profile updates when the email field changed:
<Tabs> <Tab title="Node / TypeScript"> ```typescript const conditionFn = iii.registerFunction( { id: 'conditions::emailChanged' }, async (event) => event.event_type === 'state:updated' && event.old_value?.email !== event.new_value?.email, )const fn = iii.registerFunction({ id: 'state::onEmailChange' }, async (event) => { await sendVerificationEmail(event.new_value.email) return {} })
iii.registerTrigger({ type: 'state', function_id: fn.id, config: { scope: 'users', key: 'profile', condition_function_id: conditionFn.id, }, })
</Tab>
<Tab title="Python">
```python
def email_changed(event):
if event.get('event_type') != 'state:updated':
return False
old = event.get('old_value', {})
new = event.get('new_value', {})
return old.get('email') != new.get('email')
iii.register_function({'id': 'conditions::emailChanged'}, email_changed)
def on_email_change(event):
send_verification_email(event['new_value']['email'])
return {}
iii.register_function({'id': 'state::onEmailChange'}, on_email_change)
iii.register_trigger({
'type': 'state',
'function_id': 'state::onEmailChange',
'config': {
'scope': 'users',
'key': 'profile',
'condition_function_id': 'conditions::emailChanged',
},
})
iii.register_function( RegisterFunctionMessage { id: "conditions::emailChanged".into(), description: None, request_format: None, response_format: None, metadata: None, invocation: None }, |event| async move { let is_update = event["event_type"].as_str() == Some("state:updated"); let old_email = event.get("old_value").and_then(|v| v.get("email")); let new_email = event.get("new_value").and_then(|v| v.get("email")); Ok(json!(is_update && old_email != new_email)) }, );
iii.register_function( RegisterFunctionMessage { id: "state::onEmailChange".into(), description: None, request_format: None, response_format: None, metadata: None, invocation: None }, |event| async move { let email = event["new_value"]["email"].as_str().unwrap_or(""); send_verification_email(email).await?; Ok(json!({})) }, );
iii.register_trigger(RegisterTriggerInput { trigger_type: "state".into(), function_id: "state::onEmailChange".into(), config: json!({ "scope": "users", "key": "profile", "condition_function_id": "conditions::emailChanged" }), })?;
</Tab>
</Tabs>
## State Flow
```mermaid
graph LR
W1[Worker] -->|state::set| E[Engine]
E -->|Persist| S[State Adapter]
S -->|old + new value| E
E -->|Result| W1
E -->|Match| T[Trigger Registry]
T -.->|state:updated| W2[Handler]