Why it exists
action_flows Directus collection, and executes the steps deterministically without involving the LLM again.
Result: Reliable automation from an uncensored 7B model with no cloud dependency.
Architecture
server/utils/actionRunner/index.js · server/utils/actionRunner/stepExecutors.js
The action bus integration lives in server/utils/actionBus/ and handles the BullMQ enqueue side.
ACTION tag format
The format is rigid and parseable by a simple regex — no LLM is involved in parsing:| Part | Description |
|---|---|
slug | Unique identifier of a flow in the action_flows Directus collection |
json-params | JSON object — the input parameters passed to the flow’s first step |
Examples
Flow definitions — the action_flows collection
Flows are stored as records in theaction_flows Directus collection. Each record contains:
| Field | Type | Description |
|---|---|---|
slug | string (unique) | The identifier used in ACTION tags |
name | string | Human-readable label for status messages |
active | boolean | Disabled flows are rejected immediately |
steps | JSON array | Ordered list of step definitions |
steps array has:
result_key value becomes available to subsequent steps as $result_key.field_name.
Seeded flows
The following flows are seeded in theaction_flows collection at install time:
| Slug | Purpose |
|---|---|
scout-analyze | Analyse a creator’s platform presence and surface insights |
taxonomy-tag | Run LLM-assisted taxonomy tagging on a piece of content |
post-create | Create and optionally schedule a content post |
message-generate | Draft a fan message using the creator’s persona |
memory-recall | Activate relevant skills via JIT hydration |
media-process | Enqueue a media job (watermark, clip, thumbnail) |
The 20 step types
Step executors live inserver/utils/actionRunner/stepExecutors.js. Each executor receives a config object (with interpolated variables) and an AbortSignal for the 120-second timeout.
directus_read — fetch items from Directus
directus_read — fetch items from Directus
id param) and list queries with filter, sort, fields, and limit.directus_write — create a new item
directus_write — create a new item
directus_update — PATCH an existing item
directus_update — PATCH an existing item
directus_delete — remove an item
directus_delete — remove an item
directus_search — full-text search
directus_search — full-text search
directus_trigger — fire a Directus Flow by UUID
directus_trigger — fire a Directus Flow by UUID
request operation is known-broken (returns {}). This step triggers flows via their webhook endpoint instead.591a8845 (memory-sync) · 7506f825 (taxonomy-tag) · 50a4dc14 (post-create) · a4f083fa (message-generate).stagehand_start — launch a browser session
stagehand_start — launch a browser session
sessionId.$browser.sessionId.stagehand_navigate — go to a URL
stagehand_navigate — go to a URL
stagehand_act — natural language browser action
stagehand_act — natural language browser action
stagehand_extract — extract structured data
stagehand_extract — extract structured data
stagehand_close — end the browser session
stagehand_close — end the browser session
stagehand_cookie_login — authenticate using stored cookies
stagehand_cookie_login — authenticate using stored cookies
stagehand_extract_cached — extract with cache check
stagehand_extract_cached — extract with cache check
stagehand_extract but checks extractCache first. Returns the cached result if the (sessionId, instruction) pair was recently extracted, otherwise performs a live extraction and caches the result.ollama_chat — multi-turn conversation
ollama_chat — multi-turn conversation
ollama_generate when conversation history matters.taxonomy_terms_read — cached taxonomy lookup
taxonomy_terms_read — cached taxonomy lookup
taxonomyCache. Faster than directus_read on taxonomy_mapping for hot-path flows because it bypasses Directus and reads the in-process cache.taxonomy_platform_maps_read — cached platform taxonomy maps
taxonomy_platform_maps_read — cached platform taxonomy maps
taxonomyCache.taxonomy_invalidate — bust the taxonomy cache
taxonomy_invalidate — bust the taxonomy cache
ollama_generate — single-turn LLM completion
ollama_generate — single-turn LLM completion
bullmq_enqueue — push a job to BullMQ
bullmq_enqueue — push a job to BullMQ
media-jobs BullMQ queue. The media-worker consumer picks it up and executes FFmpeg, ImageMagick, or Stagehand operations.{ queued: true, bullId, jobId }. Use media.job-status via MCP to poll completion.http_request — generic HTTP call
http_request — generic HTTP call
Variable interpolation
Step configs support variable substitution using$ prefixes. Interpolation is resolved at execution time, just before each step runs.
| Variable syntax | Resolves to |
|---|---|
$input.field | A field from the ACTION tag’s JSON params |
$steps[N].field | The result of step N (zero-indexed) |
$result_key.field | The named result of a previous step |
$context.field | Extra context injected by genieChat (user info, workspace) |
End-to-end example: “scrape my OnlyFans stats”
A creator sends this message in the chat interface. Here is what happens:Agent emits ACTION tag
genieChat intercepts the tag
[ACTION:...] pattern. When found, the tag is extracted and passed to ActionFlowExecutor.run("scrape-profile", { platform: "onlyfans", creator_profile_id: "cp-123" }).Flow definition fetched from Directus
action_flows for slug = "scrape-profile". It retrieves a 5-step flow definition: read credentials → start browser → inject cookies → navigate → extract stats.Steps execute sequentially
directus_read— fetches theplatform_sessionsrecord for this creatorstagehand_start— launches a headless browser, returnssessionIdstagehand_navigate— navigates tohttps://onlyfans.com/my-handle/statsstagehand_extract— extracts subscriber count, earnings, post count as JSONdirectus_write— writes extracted stats toplatform_stats_snapshots
BullMQ job enqueued
bullmq_enqueue step creates a scrape_post_performance job so individual post metrics are fetched asynchronously without blocking the chat response.Error handling
- Each step has a 120-second hard timeout enforced via
AbortController. - If any step fails, the executor returns
{ success: false, failedStep: N, error: "..." }immediately — subsequent steps do not run. - Timeout and failure messages are surfaced via
onStatusto the SSE stream, so the creator sees a meaningful error rather than a stalled spinner. - Disabled flows (
active: false) are rejected before any steps run.