Factuarea APIDevelopers

Automations

The when, if and do model of the rule engine — versions, the dry run that materialises nothing, run history and steps, replay, the engine limits and the portfolio rules of an account.

The automation engine turns an event into work. A rule listens to one event type, a condition decides whether that particular event is its case, and an ordered list of actions runs. Everything hangs from https://api.factuarea.com/v1/companies/{company}/automations, needs the automations plan module and uses four fine-grained scopes — automations:read, automations:write, automations:delete and automation_runs:read. See Scopes & permissions.

When, if and do

A rule has exactly three moving pieces:

PieceFieldWhat it decides
Whentrigger_typeThe event the rule listens to, in resource.action form (invoice.paid)
IfconditionsA condition tree over the fields of that event. An empty {} means no condition: the rule always acts
DoactionsThe actions to run when an event matches, in the order you declare them

Ask the catalog instead of guessing. GET /v1/companies/{company}/automations/catalog returns the triggers your company can subscribe to — already filtered by the modules your plan includes, so a trigger governed by a module you have not contracted is not offered at all — plus the closed set of operators and combinators a condition accepts and the actions that have a registered adapter in this deployment. It is deterministic: two consecutive calls with no configuration change return the same body, so cache it and diff it.

The evaluable fields of a trigger are a second call. They are deliberately out of the catalog: around a hundred visible triggers with dozens of properties each would make a document of hundreds of kilobytes to end up using the fields of one. Ask GET /v1/companies/{company}/automations/catalog/triggers/{trigger}/fields for the trigger you picked, and build the condition against the fields it lists.

Twelve action types exist today: notify_in_app, notify_channel, emit_webhook, create_calendar_event, send_document_email, send_payment_reminder, change_status, tag_entity and, for tasks, create_task, change_task_status, assign_task and add_task_comment — see Tasks and projects. The catalog announces only the ones with a wired adapter, never the declared type list — picking a type without an adapter would produce a rule whose step is skipped at run time. create_calendar_event is retired: the calendar it fed no longer exists, so the name stays in the vocabulary — a version sealed earlier still reads — but no adapter runs it and its step produces no effect. Use create_task instead.

The type enum of an action grew. If your integration validates actions[].type against a closed list, add create_task, change_task_status, assign_task and add_task_comment, and keep create_calendar_event recognizable so that older rule versions still parse.

A new rule is born inactive. Creating one seals version 1 and leaves it in draft: it hears nothing until you activate it with POST /v1/companies/{company}/automations/rules/{rule}/activate. Activation checks the plan’s active-rule cap. It does not pre-check your monthly execution budget: that is checked when a run is admitted. Read the usage endpoint for both counters before activating.

curl -X POST https://api.factuarea.com/v1/companies/{company}/automations/rules \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Warn me on a large payment",
    "scope": "empresa",
    "trigger_type": "invoice.paid",
    "conditions": { "field": "total", "operator": "gte", "value": 1000 },
    "actions": [
      {
        "type": "notify_in_app",
        "order": 0,
        "parameters": {
          "recipient_type": "role",
          "role": "admin",
          "category": "invoice",
          "title": "Large payment",
          "message": "An invoice over 1000 EUR has been paid"
        }
      }
    ]
  }'

Tasks and projects

Automations reach the task module in both directions: its events start rules, and four actions create and change tasks. Both need the tasks module.

Triggers

The 21 task and project events — task.*, task_comment.*, task_time_entry.* and project.* — are valid triggers. Like every trigger governed by a module, they exist only for a company with the tasks module: the catalog does not offer them otherwise, and saving a rule that uses one returns 422. Conditions read the fields each event publishes — for a task, among others its status, column, priority, project, assignee, due date and completion date; ask GET /v1/companies/{company}/automations/catalog/triggers/{trigger}/fields — and the run freezes the data.object of the event as its payload.

Actions

ActionWhat it doesParameters
create_taskCreates a taskproject_id (required: an active project of your company), title (required, up to 500 characters), description (Markdown), priority, assignee_mode (user, role or trigger_actor) with assignee_user_id or assignee_role (owner, admin or member), label_ids, the due date and link_to_trigger
change_task_statusMoves the task of the triggertarget: the id of a column of the task's project, or planned or archived
assign_taskAssigns the task of the triggerassignee_user_id: a member of your company
add_task_commentComments on the task of the triggerbody: Markdown, from 1 to 10,000 characters
{
  "name": "Follow up approved quotes",
  "scope": "empresa",
  "trigger_type": "quote.approved",
  "conditions": {},
  "actions": [
    {
      "type": "create_task",
      "order": 0,
      "parameters": {
        "project_id": "0193a4f2-7c20-7a11-8b52-4d6e8f0a2c01",
        "title": "Prepare the delivery of the approved quote",
        "priority": "high",
        "assignee_mode": "role",
        "assignee_role": "admin",
        "link_to_trigger": true
      }
    }
  ]
}
  • The task of the trigger. The last three actions act on it: the task itself in task.* events, and the task a comment or a time entry belongs to in task_comment.* and task_time_entry.* events. With any other trigger, or if the task no longer exists, the step is discarded with task_target_not_found.
  • Due date of create_task. Send either due_on (an explicit date) or due_from_payload_path with due_offset_days (a date field of the frozen payload plus a number of days) — never both. A field that is missing or is not a date fails the step with a message that names it: no date is invented. With neither, the task has no due date.
  • link_to_trigger links the new task to the entity that fired the rule — an invoice, a quote, a contact… — so it appears among that entity's linked tasks. If the trigger has no linkable entity, the task is created unlinked and the step trace says so.
  • Assignee. assignee_mode: "trigger_actor" assigns the user who caused the event, and leaves the task unassigned when the event does not identify a member. role picks the first user of the company with that role, in a deterministic order. A user who is not a member of the company fails the step and creates nothing.
  • Comments by an automation have no user as author (their author.type is automation); nobody can edit them and an admin can delete them.
  • References are checked against your company — the project, the assignee, the labels and a target that is a column — when the rule is saved (422, with the same message whether the reference belongs to another company or does not exist) and again when it runs.
  • Discarded steps. A create_task whose project was archived after the rule was saved is discarded with task_project_not_found, and can be replayed once you unarchive it; a company that lost the tasks module gets module_not_accessible, replayable when the module is back.
  • Loops are cut. A rule task.created → create_task reaches the chain-depth cap like any other chain.
  • The dry run validates parameters, references, subject and module exactly like a real run, and creates no task and no comment.

Every edit seals a version

Editing a rule never rewrites its previous definition. It seals a new version and bumps current_version, and every run keeps a pointer to the exact version it executed, so a run from weeks ago stays readable after the rule has moved on.

  • PATCH /v1/companies/{company}/automations/rules/{rule} updates partially: omitted fields keep their value, and "" in description clears it. The status is left untouched — editing an active rule leaves it active, running the new version from its next event onwards.
  • Pausing and activating touch no definition, so they seal no version.
  • GET /v1/companies/{company}/automations/rules/{rule}/versions lists the sealed versions and .../versions/{version} returns one, with the condition tree and the actions frozen as they were.

The rule_version of a run points at exactly that number. It can lag behind current_version, and that is the point.

The dry run materialises nothing

POST /v1/companies/{company}/automations/rules/{rule}/dry-run answers what a rule would do against a sample event. It executes nothing: no e-mail leaves, no webhook delivery is created, no entity is mutated, no run or step row is written, and neither the monthly budget nor the engine's frequency limit is consumed. That is why it takes the read scope even though it is a POST — the sample event has to travel in the body.

The answer carries matched, one condition_trace entry per evaluated leaf in evaluation order, and one steps entry per action with what it would resolve to. A condition that cannot be evaluated comes back as condition_error with a 200, not as an HTTP failure: seeing why a rule cannot be evaluated before activating it is exactly what the dry run is for.

If the rule acts on documents — send a document by e-mail, send a payment reminder, change a status, tag an entity — pass event_aggregate_id with the id of a real document of yours. Without it the dry run reports that no step would run, and a perfectly correct rule looks broken.

curl -X POST https://api.factuarea.com/v1/companies/{company}/automations/rules/{rule}/dry-run \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "invoice.paid",
    "event_payload": { "total": 1200, "status": "paid" }
  }'

Runs and their steps

Every admitted event produces a run, and every action of the frozen definition produces a step. A run carries the definition it executed (rule_version + rule_snapshot) and the payload of the event that triggered it (event_payload), both frozen at execution time.

Run statusWhat it means
pendingQueued, not started
runningIn flight
completedEvery step applied its effect
failedFinished without producing its effect, and not reprocessable
dead_letteredParked, waiting for a manual replay
blockedStopped by an engine limit before executing

Steps come back always in execution order, because step_index — zero-based — is the identity of a step: it is what you replay and what the automation_run.step_dead_lettered event publishes as data.step_index. Each step carries its action_type, its frozen parameters, the result its adapter returned and a replayable flag.

Two orthogonal axes explain an outcome. discard_reason is the classified reason, from a closed catalog — condition_not_matched, chain_depth_exceeded, rate_limit_exceeded, monthly_budget_exhausted, step_attempts_exhausted… — so branch on it. last_error is the raw technical message, meant for debugging. discard_reason_label is the human label of the reason and is always in Spanish: it belongs to the vocabulary of the engine and does not follow Accept-Language.

Read the history with GET /v1/companies/{company}/automations/runs — filter by automation_rule_id, status or a created[…] window —, GET /v1/companies/{company}/automations/runs/{run} and GET /v1/companies/{company}/automations/runs/{run}/steps. All three paginate by cursor; see Pagination.

Replay executes the work again, for real

Two operations rearm parked work: POST /v1/companies/{company}/automations/runs/{run}/replay rearms every parked step of a run, and POST /v1/companies/{company}/automations/runs/{run}/steps/{step_index}/replay rearms a single one, leaving its siblings with their state, their reason and their timestamps untouched.

The rearmed steps execute for real: they send e-mail, deliver webhooks and call third parties. This is not an inert retry — confirm with the account owner before calling it.

  • Only steps parked with a replayable reason are rearmed. A run still in flight, or one that finished cleanly, is rejected with 422 automation_replay_not_allowed.
  • The response is 202 — accepted and queued, not finished — and its body carries ids only, deliberately without a status, because "queued" is not one of the run statuses. Check the outcome with the run, or more cheaply with its steps.
  • Calling it twice does not duplicate the effect: the engine rechecks the state of each step inside its own lock.
  • step_index is the index published by the steps endpoint, not the position of the action in the rule definition. An index outside the range returns 404, exactly like a run that is not yours.

The limits of the engine

The plan bounds how many rules you can activate. Separately, four guardrails bound execution: a run stopped by one of those execution limits is recorded as blocked with its typed reason — blocked is not failed, and reading the reason tells the two apart. Rejection at the active-rule cap creates no run.

LimitWhat it boundsReason and error code
Active automation rulesActive, non-deleted rules of your company; checked when activating or resumingActivation rejected · 402 automation_active_rules_limit_exceeded
Chain depthHops "action → event → rule", three by defaultchain_depth_exceeded · 422 automation_chain_depth_exceeded
Frequency limitRuns admitted per minute and company. A brake on peaks, not on volumerate_limit_exceeded · 429 automation_rate_limit_exceeded
Monthly budgetRuns the plan allows per periodmonthly_budget_exhausted · 402 automation_monthly_budget_exhausted
Consecutive failuresA streak of failed runs pauses the rule by itselfEvent automation_rule.auto_paused with consecutive_failures

An event produced by a person or by an integration enters at depth zero, so only what an automation itself caused spends the chain budget. A rule that reacts to automation_run.failed and fails again therefore cannot loop forever.

GET /v1/companies/{company}/automations/usage is where you stand against the monthly budget: consumed, limit (null means no automatic ceiling, never "unknown" — treat it as no progress bar, not as zero), reset_at already resolved, the period as YYYY-MM and the threshold_percent at which the platform starts warning the account owner. That percentage travels so your warning and ours cannot drift apart.

The same response also carries active_rules: {count, limit}: count is the company’s active, non-deleted rules; drafts and paused rules do not count. limit is the plan cap, including zero when no activation is permitted, or null when there is no automatic ceiling. Enterprise capacity is agreed by contract. The public v1 response and MCP get_automation_usage expose the same pair, without limit_mode.

Response fragment:

{
  "data": {
    "active_rules": { "count": 7, "limit": 7 }
  }
}

When count >= limit for a fixed cap, activating or resuming a rule is rejected with automation_active_rules_limit_exceeded: v1 returns 402 with error.type: payment_required_error and error.details: {active_rules, limit}. Pause an active rule with POST /v1/companies/{company}/automations/rules/{rule}/pause or upgrade the plan before trying again. MCP activate_automation_rule returns JSON-RPC -32008, with the same code in data.code, data.http_status: 402 and the same pair in data.details; the app reports HTTP 422 with the same code.

Rules already active above the cap keep running and can still be edited or paused. This cap does not pause them automatically; it only prevents further activations. Pausing frees one active-rule place, and resuming checks the cap again. The monthly execution budget remains separate and is checked on execution.

A single step also has a cap on attempts. When it runs out, the step parks as dead_lettered with step_attempts_exhausted and waits for a replay — that is the only state that allows re-entry.

Portfolio rules

A rule declares which tax IDs it watches in its optional scope field: empresa — the value it takes when you omit it — watches the events of the company that owns it, and cartera watches the events of the portfolio of the account that owns it, and delivers the notice to that company, the titular NIF of the account. They were designed for accounting firms (gestorías), which are accounts like any other.

The scope is immutable. You choose it when you create the rule, and to change it you create another rule. An update that sends a different value is rejected with automation_rule_scope_immutable; send the scope the rule already has, or omit the field.

What a cartera rule watches. The other real tax IDs of the account, plus the tax IDs that another account puts within its reach through a relation in force at level operate, a granted access or a managed account. A relation at level view, or one that was revoked or has ended, brings nothing in. The rule never watches the company that owns it: if you want the same automation for that company, create it with scope empresa.

The universe is not a stored list. It is worked out from each event, and it is checked again at every step: when the candidate rules of an event are chosen, before the run is written, inside each step before its action runs, and when you replay a run or a step. A tax ID that has been archived, deactivated or transferred to another account, or whose relation was revoked, ended or lowered to view, fails the check: no run is written for it or, if the run already exists, the step stops as blocked with reason tenant_mismatch and its action never runs. Replaying does not get around it.

Who can create a cartera rule. Only the titular company of an account that has the gestoria module granted, by plan (empresario and enterprise) or by a company override. The first thing asked is whether the company is the titular of its account: an additional tax ID —one the account bought, received by transfer or registered— never has a portfolio of its own, even if the module was granted to it, because the portfolio belongs to the account and its titular represents it. You do not need a second tax ID to write the rule; until the account has one, it simply never fires.

The catalog returns both scopes always, each with available and, when it is not, an unavailable_reason: module_not_granted (upgrading the plan grants it) or company_is_managed_child (the company is not the titular of its account, so no plan unlocks it; the name is inherited and stays because it is part of the contract). That is how you tell "not contracted" from "does not exist". Creating a cartera rule without that right returns 403 automation_portfolio_scope_not_available.

Only five of the twelve action types are accepted in cartera. notify_in_app, notify_channel and emit_webhook notify the titular company or touch no entity at all, and create_task creates the task in a project of the titular company itself — link_to_trigger is rejected in cartera, because the entity that fired the rule belongs to another tax ID. The retired create_calendar_event keeps its place in this group but does nothing. The other seven — send_document_email, send_payment_reminder, change_status, tag_entity, change_task_status, assign_task and add_task_comment — would take a document or a task of another tax ID as their subject, which the multi-tenant boundary does not allow. They are rejected when the rule is created or edited, never at run time, with 422 automation_portfolio_scope_forbids_action. Each action of the catalog carries supports_portfolio_scope so you can filter before writing the rule.

Whose run it is, and its subject. A portfolio run is stored under the titular company of the account: it is its rule, its notices land in its inbox and its runs consume its monthly budget and its frequency limit, so the budget is spent in proportion to the portfolio. One event can start more than one run when its tax ID is in the portfolio of more than one titular, for example a tax ID granted to another account at level operate: each titular with a matching rule gets its own run, against its own budget. Each run names in subject_company the tax ID it acted upon, which is null for empresa runs. The block travels even when that tax ID can no longer be read: id and name come back null instead of the block disappearing. The tax ID itself does not see the portfolio runs of its titular: they belong to the titular and hold the frozen definition of its rule.

Filter runs with GET /v1/companies/{company}/automations/runs?subject_company_id=… and rules with GET /v1/companies/{company}/automations/rules?scope=cartera. Filtering by a tax ID outside the portfolio returns an empty page, never an error, so the filter cannot be used to find out which tax IDs exist.

In test mode nothing leaves the sandbox

A fact_test_ key operates against an isolated sandbox company, and there the engine behaves the same up to the last instant: the trigger matches, the run is admitted and recorded, and every step is neutralised right before producing its effect. The step closes with sandbox_neutralized and the run finishes completed — a neutralised effect is the intended outcome, not a failure. No e-mail is sent, no webhook is delivered and no document changes. See Test mode & sandbox.

Next steps

On this page

Need a hand?Contact support