⚡ XActions
📖 Guides & Reference

Workflow Engine

Declarative JSON pipelines with triggers, conditions, and chained actions. Automate multi-step Twitter operations without code.

Overview

The workflow engine lets you define automation pipelines as JSON:

  • Steps execute sequentially, passing data through a shared context
  • Triggers start workflows automatically (cron, interval, webhook, event)
  • Conditions are their own step type, and gate everything after them
  • Actions are the building blocks: scrape, post, follow, transform, export

Reach it three ways: the CLI (xactions workflow), the MCP tools (x_workflow_create, x_workflow_run, x_workflow_list, x_workflow_actions), or the API server. The module is not published as a package subpath, so a Node import is by relative path from a clone of the repo.


Quick Start

CLI

xactions workflow actions                       # every action, with its params
xactions workflow create --file my-flow.json    # or omit --file for the prompts
xactions workflow list
xactions workflow run morning-engagement --auth "$X_AUTH_TOKEN"
xactions workflow runs <workflowId> --limit 10
xactions workflow delete <id>

Node.js

import workflows from './src/workflows/index.js';

// Define a workflow
const definition = {
  name: 'morning-engagement',
  description: 'Like tweets from my niche every morning',
  trigger: { type: 'schedule', cron: '0 9 * * *' },  // 9 AM daily
  steps: [
    {
      action: 'searchTweets',
      params: { query: 'AI startup', limit: 10 },
      output: 'tweets',
    },
    {
      action: 'like',
      params: { url: '{{tweets.0.url}}' },
      onError: 'continue',
    },
  ],
};

// Validate before you save. `errors` names exactly what is wrong.
const { valid, errors } = workflows.validate(definition);
if (!valid) throw new Error(errors.join('; '));

// Create (saves + registers the trigger)
const workflow = await workflows.create(definition);

// Or run one immediately, without saving
const run = await workflows.run(definition, { authToken: process.env.X_AUTH_TOKEN });

console.log(run.status);    // 'completed'
console.log(run.steps);     // one entry per step

The named exports are the same functions (create, get, list, update, remove, run, runs, getRun, listActions, registerAction, executeAction, evaluateCondition, getAvailableOperators, initTriggers, shutdown), with one difference: validate is only on the default export, and is named validateWorkflow when imported by name.

MCP (AI Agents)

"Create a workflow that scrapes @elonmusk's tweets every hour and analyzes sentiment"
→ Uses x_workflow_create tool

"Run my morning-engagement workflow"
→ Uses x_workflow_run tool

"Show me all my workflows"
→ Uses x_workflow_list tool

"What actions can I use in workflows?"
→ Uses x_workflow_actions tool

API

# Create a workflow
curl -X POST http://localhost:3001/api/workflows \
  -H "Content-Type: application/json" \
  -d '{"name": "my-flow", "steps": [{"action": "scrapeProfile", "params": {"target": "elonmusk"}}]}'

# Run a workflow (by id or name)
curl -X POST http://localhost:3001/api/workflows/my-flow/run

# List workflows, and every available action
curl http://localhost:3001/api/workflows
curl http://localhost:3001/api/workflows/actions

# Execution history, and one run
curl http://localhost:3001/api/workflows/my-flow/runs
curl http://localhost:3001/api/workflows/my-flow/runs/<runId>

# Fire a webhook-triggered workflow
curl -X POST http://localhost:3001/api/workflows/webhook/<webhookId> -d '{}'

Workflow Definition

{
  name: 'string (required)',          // Unique workflow name
  description: 'string',              // Human-readable description
  trigger: {                          // How the workflow starts
    type: 'schedule|cron|interval|webhook|event|manual',
    // type-specific config (see Triggers section)
  },
  steps: [                            // Sequential steps
    // An ACTION step
    {
      action: 'string',               // Action name (see Actions)
      params: { ... },                 // Action parameters
      output: 'string',               // Save the result under this context key
      onError: 'stop|continue',       // On a thrown error (default: 'stop')
    },
    // A CONDITION step
    {
      condition: 'tweets.length > 0', // Or the structured form, see Conditions
      onFail: 'stop|skip',            // When it does not pass (default: 'stop')
    },
  ],
}

validate() requires name (a string) and steps (an array), and every step must carry either action or condition. A trigger object must have a type, and a schedule trigger must have a cron.

A step is either an action or a condition, never both. If a step carries both keys the condition runs and the action is silently skipped, because the engine branches on condition first. Put the gate in its own step, immediately before the work it guards.

Context and variable passing

Every step with an output writes its result into a shared context object. Later steps read it with {{...}} interpolation, using dot and index paths.

{
  steps: [
    {
      action: 'scrapeProfile',
      params: { target: 'elonmusk' },
      output: 'profile',              // context.profile
    },
    {
      action: 'scrapeTweets',
      params: { target: '{{profile.username}}', limit: 20 },
      output: 'tweets',
    },
    {
      condition: 'tweets.length > 0',
      onFail: 'skip',                 // keep going even if there are none
    },
    {
      action: 'postTweet',
      params: { text: 'Found {{tweets.length}} tweets from @elonmusk' },
    },
  ],
}

The context also carries three reserved keys the engine sets for you: _workflow ({ id, name }), _run ({ id, trigger }) and _timestamp. authToken is in the context too, and is stripped from the stored run record along with anything else whose key starts with _.


Triggers

Schedule (Cron)

{ type: 'schedule', cron: '0 9 * * *' }    // 9 AM daily
{ type: 'cron', cron: '*/30 * * * *' }     // Every 30 minutes

Uses Bull queue repeatable jobs. Cron syntax: minute hour day month weekday.

Interval

{ type: 'interval', ms: 300000 }           // Every 5 minutes

Uses setInterval. Lighter than cron but less reliable across restarts.

Webhook

{ type: 'webhook' }
// Generates URL: /api/workflows/webhook/{webhookId}

POST to the generated URL to trigger the workflow. Payload is passed as initial context.

Event

{ 
  type: 'event', 
  event: 'new_tweet',      // or 'follower_change'
  threshold: 5              // Trigger after 5 events
}

Watches for streaming events and triggers when threshold is met.

Manual

{ type: 'manual' }

No automatic trigger. Invoke it explicitly with run(), xactions workflow run, or POST /api/workflows/:id/run. A workflow with no trigger at all behaves the same way.


Actions

31 actions ship built in. xactions workflow actions prints the live list with every parameter; the tables below are that list at the time of writing. A * marks a required parameter.

The account parameter is called target, not username, and the tweet parameter is url, not tweetUrl. A step naming the wrong key fails at run time with a missing-parameter error.

Scrapers

Action Params Description
scrapeProfile target* Profile with bio, stats and recent tweets
scrapeFollowers target*, limit Follower list
scrapeFollowing target*, limit Following list
scrapeTweets target*, limit An account's tweets
searchTweets query*, limit Search results
scrapeHashtag hashtag*, limit Tweets for a hashtag
scrapeTrending none Trending topics
scrapeThread url* A full thread or conversation
scrapeMedia target*, limit Images and video from an account
scrapeBookmarks limit Your bookmarks (needs a session)
scrapeNotifications limit Your notifications (needs a session)
scrapeListMembers url*, limit Members of a list
scrapeLikes url*, limit Accounts that liked one tweet

Automation

Action Params Description
follow target* Follow an account
unfollow target* Unfollow an account
postTweet text* Post a tweet
like url* Like a tweet
retweet url* Repost a tweet
reply url*, text* Reply to a tweet
getNonFollowers target*, limit Accounts you follow that do not follow back

Transform

input is the name of a context key, not the value itself.

Action Params Description
filter input*, field*, operator*, value* Filter an array by a condition
count input* Count items in an array
pick input*, fields* Keep only these fields on each object
slice input*, start, end Take a subset of an array

AI

Action Params Description
summarize input*, provider, model, prompt Summarize text with OpenRouter or a local LLM
generateText prompt*, system, model Generate text

Utility

Action Params Description
log message, variable Print a message or a context value while debugging
delay ms* Wait
exportJSON input*, filepath* Write a context value to a JSON file
exportCSV input*, filepath* Write a context value to a CSV file
template text* Render a {{variable}} string against the context

Custom Actions

Register your own actions:

import { registerAction } from './src/workflows/index.js';

registerAction('sendDiscordWebhook', {
  description: 'Send a message to Discord',
  category: 'notification',
  params: {
    webhookUrl: { type: 'string', required: true },
    content: { type: 'string', required: true }
  },
  execute: async (params, context) => {
    await fetch(params.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: params.content })
    });
    return { sent: true };
  }
});

Plugin actions are also available — see plugins.md.


Conditions

A condition is a step of its own. When it passes, the workflow continues. When it does not, onFail: 'skip' moves to the next step and the default stops the run, marking it completed with a result.stoppedAtCondition naming the step.

Four forms are accepted:

{ condition: 'tweets.length > 0' }                                 // expression string
{ condition: { left: 'tweets.length', operator: '>', right: 0 } }  // structured
{ condition: { all: ['tweets.length > 0', 'profile.followers > 100'] } }  // AND
{ condition: { any: ['tweets.length > 0', 'media.length > 0'] } }        // OR

The structured form uses left / operator / right. There is no field / value form, and a condition written that way is reported as an invalid condition format and treated as not passing.

Available Operators

getAvailableOperators() returns the live list. There are no eq / gt style aliases; use the symbols.

Operator Description Example
== Equals, compared as strings status == "active"
!= Not equals error != "null"
> Greater than, compared as numbers tweets.length > 10
>= Greater or equal score >= 0.8
< Less than errors < 3
<= Less or equal retries <= 5
contains Case-insensitive substring profile.bio contains "founder"
not_contains The inverse profile.bio not_contains "spam"
matches Case-insensitive regular expression tweet.text matches "^gm"
exists Not null and not undefined profile.bio exists
empty Null, empty string, or empty array tweets empty
not_empty The inverse tweets not_empty

The right-hand side is resolved against the context first, so a bare word is read as a context path. Quote a literal string ('active' or "active"). Numbers, true, false and null are recognised as literals, and so are duration strings (30m, 1h, 2d), which resolve to milliseconds. An expression with no operator is treated as an exists check.


Execution Runs

Every workflow execution produces a run record:

{
  id: '6c3f...-uuid',
  workflowId: 'wf_xyz',              // 'anonymous' when run() was given a bare definition
  workflowName: 'morning-engagement',
  status: 'completed',               // 'running' | 'completed' | 'failed' | 'cancelled'
  trigger: 'manual',                 // a string, whatever options.trigger was
  userId: 'system',
  startedAt: '2026-02-25T09:00:00.000Z',
  completedAt: '2026-02-25T09:00:15.000Z',
  stepsCompleted: 2,
  totalSteps: 2,
  steps: [
    {
      index: 0,
      type: 'action',                // 'action' | 'condition'
      name: 'searchTweets',
      status: 'completed',           // 'completed' | 'skipped' | 'failed'
      startedAt: '...',
      completedAt: '...',
      result: { /* summarised, large arrays are truncated */ },
      error: null,
    },
  ],
  context: { tweets: [/* ... */] },  // authToken and _-prefixed keys removed
  error: null,
  result: null,
}

A run that stops at a condition is completed, not failed, and its result is { stoppedAtCondition, reason, context }. A step that throws with onError: 'continue' is recorded as failed while the run carries on.

Query Runs

import workflows from './src/workflows/index.js';

// Get all runs for a workflow
const runs = await workflows.runs('morning-engagement', 50);

// Get a specific run
const run = await workflows.getRun('morning-engagement', '6c3f...-uuid');

API Reference

High-Level API

Function Signature Description
create(definition) (Object) → Promise<Object> Save workflow + register trigger
get(idOrName) (string) → Promise<Object|null> Lookup by ID or name
list() () → Promise<Object[]> All saved workflows
update(id, updates) (string, Object) → Promise<Object> Update + re-register triggers
remove(id) (string) → Promise<boolean> Delete + unregister triggers
run(idOrNameOrDef, options?) → Promise<Object> Execute a workflow
runs(workflowId, limit?) → Promise<Object[]> Execution history
getRun(workflowId, runId) → Promise<Object> Specific run
validate(definition) (Object) → { valid, errors[] } Validate a workflow. Named validateWorkflow in the named exports.
listActions() () → Object[] All available actions
registerAction(name, def) (string, Object) → void Register custom action
initTriggers(options?) (Object) → void Initialize trigger system
shutdown() () → Promise<void> Clean up

Run Options

Option Type Description
trigger Object Override trigger data
initialContext Object Seed the context
authToken string Twitter auth token
userId string User ID for tracking
onProgress(event) Function Progress callback
isCancelled() Function Cancellation check

Example Workflows

Competitor Monitor

{
  name: 'competitor-monitor',
  trigger: { type: 'schedule', cron: '0 */6 * * *' },
  steps: [
    { action: 'scrapeProfile', params: { target: 'competitor1' }, output: 'profile' },
    { action: 'scrapeTweets', params: { target: 'competitor1', limit: 20 }, output: 'tweets' },
    { condition: 'tweets not_empty', onFail: 'skip' },
    { action: 'summarize', params: { input: 'tweets', prompt: 'What are they pushing this week?' }, output: 'summary' },
    { action: 'exportJSON', params: { input: 'summary', filepath: './competitor-summary.json' } },
  ],
}

summarize and generateText route through OpenRouter by default and need OPENROUTER_API_KEY. For sentiment scoring specifically, the analytics module is the better tool: analyzeSentiment is not a workflow action. See analytics.md.

Growth Automation

{
  name: 'niche-engagement',
  trigger: { type: 'interval', ms: 1800000 },
  steps: [
    { action: 'searchTweets', params: { query: 'AI agents', limit: 5 }, output: 'tweets' },
    { condition: 'tweets not_empty' },
    { action: 'like', params: { url: '{{tweets[0].url}}' }, onError: 'continue' },
    { action: 'follow', params: { target: '{{tweets[0].author}}' }, onError: 'continue' },
  ],
}

Write steps go through the same account safety net as everything else: keep the interval conservative, and remember the MCP server enforces a daily per-action budget on top (getting-started.md).

⚡ Free and open source

No API keys, no monthly fees, no signup. Star the repo if it saved you a subscription.

View on GitHub