Description
n8n-nodes-aca
An n8n community node for ACA – Automated Client Acquisition.
ACA is a multi-tenant outbound platform: CRM contacts, lead lists, email sequences, and a unified inbox across LinkedIn, email, WhatsApp, Instagram, Telegram and SMS. This package gives you two nodes:
- ACA – read and write contacts, lists, sequences, enrollments and conversations, and act on them.
- ACA Trigger – start a workflow when something happens in ACA – including buying signals, with the signal attached – with signature verification.
Installation | Credentials | ACA node | ACA Trigger | Example workflow | Things worth knowing
Installation
n8n Cloud – Settings > Community nodes > Install, then enter n8n-nodes-aca.
Self-hosted – the same screen, or from the command line:
npm install n8n-nodes-aca
Restart n8n afterwards if you installed manually.
Credentials
The node authenticates with an ACA API token.
1. In ACA, open Settings > CLI Tokens.
2. Create a token. It is shown once – copy it then.
3. In n8n, add an ACA API credential and paste it into API Token.
4. Click Test. A green check means the token is valid and reaches your organisation.
A token is tied to one organisation at the moment you issue it. Switching organisations in the ACA app later does not change what an existing token can see, so if you automate more than one organisation, create one token per organisation and one n8n credential per token.
Tokens are stored as a hash and cannot be recovered. If you lose one, revoke it and issue another.
Rate limit: 120 requests per minute per token. Every response carries X-RateLimit-Remaining; exceeding it returns 429 with a Retry-After.
ACA node
| Resource | Operations |
| — | — |
| Contact | Get, Get Many, Create, Update, Delete, Delete Many |
| Lead List | Get, Get Many, Create, Archive, Get Contacts, Add Members, Remove Members |
| Sequence | Get, Get Many |
| Enrollment | Get, Get Many, Create (enroll contacts or whole lists) |
| Conversation | Get, Get Many, Get Messages |
| Message | Send (reply into an existing conversation) |
| Custom Field | Get Many, Create |
| Event Action | Add Tag, Remove Tag, Add Note, Change Stage, Update Score, Send Message, Enroll in Sequence, Remove From Sequence, Set AI Handling, Assign to User |
Get Many operations support Return All, which follows ACA’s cursor pagination for you, and a Filters collection that maps to the API’s own filters.
Filtering a lead list
Lead List > Get Contacts carries the full filter set, so you narrow the list on
ACA’s side rather than pulling everything and filtering in the workflow.
The three switches people reach for most – Has Email, Has LinkedIn URL,
Has Phone – are shortcuts over one general mechanism. Field Conditions
exposes it directly: pick any contact field and ask whether it is filled,
empty, equals or contains a value. Conditions combine with AND.
Has LinkedIn URL: true
Field Conditions: company contains "recruit"
Email Quality: clean
Also available: search across name/email/company/title, countries, cities,
industries, company sizes, seniorities, departments, detected and excluded
technologies, ICP and decision-maker score ranges, and AI analysis status.
These run through the same Advanced Filters engine the ACA app uses, so a filter
means the same thing in both places.
Event Action vs the REST resources
They overlap on purpose, and the difference matters:
Contact > Update replaces the entire tag list, so adding one tag that way means reading the contact first and writing every tag back.ACA Trigger
Pick the events you care about and activate the workflow. The node registers its own webhook subscription with ACA, and removes it when the workflow is deactivated – there is nothing to paste into ACA’s settings.
Events
| Event | Fires when |
| — | — |
| contact_created | A contact is created |
| contact_updated | Any field on a contact changes |
| stage_changed | A contact moves to a different pipeline stage |
| score_changed | A contact’s lead score changes |
| message_received | An inbound message arrives, on any channel |
| message_sent | An outbound message goes out |
| tagadded / tagremoved | A tag is added to or removed from a contact |
| sequence_completed | A lead reaches the end of an email sequence |
| lead_replied | An enrolled lead replies, and the sequence stops for them |
| handoff_requested | A human takes over a conversation from the AI |
| listmemberadded | A contact is added to a lead list |
| listmemberremoved | A contact is removed from a lead list |
| signalleadadded | A buying signal puts a lead on a lead list — see Buying signals |
Narrowing to one lead list
Select listmemberadded or listmemberremoved and a Lead List Names or IDs picker appears. Pick one or more lists and only those fire the workflow; leave it empty for every list.
The filter is registered with ACA, not applied in n8n – events for the lists you did not pick are never queued and never delivered, so a 250,000-contact build into some other list costs your workflow nothing.
Every list event carries the list it happened in, so you can branch on it too:
{{ $json.data.list_name }} // "Q3 SaaS founders"
{{ $json.data.list_id }}
Output
By default the node emits a flattened shape:
{
"event": "lead_replied",
"timestamp": "2026-08-11T12:56:42.801Z",
"deliveryId": "6b1f0c2a-9d3e-4f58-a71b-2c8d4e5f6a7b",
"organizationId": "afb808d4-0000-0000-0000-000000000000",
"data": { "...": "event-specific" }
}
Turn on Options > Raw Envelope to get ACA’s delivery body verbatim instead.
Delivery guarantees
Delivery is at-least-once. ACA retries a failed delivery up to three times, roughly a minute apart, reusing the same X-Webhook-ID – surfaced as deliveryId. If your workflow does anything that must not happen twice, deduplicate on that value.
Every delivery is signed. The node verifies X-Webhook-Signature (HMAC-SHA256 of the raw body) against the secret ACA issued when the subscription was created, and rejects anything that does not match with a 401. An event you did not select is acknowledged with a 200 and starts nothing, so ACA does not retry it.
Buying signals
Select the signalleadadded event and the workflow starts when a buying signal puts a lead on a lead list — a funding round, an acquisition, a hiring spike, a leadership move. One execution per lead, with the signal that caused it attached.
It covers every way a signal becomes a lead in ACA: public-signal auto-enroll rules, your own custom signal subscriptions, and pushes from the signal library. signal_source tells you which one.
A Signal Types picker appears with the event, limiting the workflow to funding, hiring, hiring demand, acquisition, job change, growth or executive hire. Leave it empty for all of them. Unlike the lead-list filter, this one runs inside n8n — ACA still queues and delivers the event, the node acknowledges it with a 200 and starts nothing. Use it for readability, not to save ACA work.
Signal events are the one exception to the output shape above: the signal is flattened to the top level, so there is no data to reach through:
{
"event": "signalleadadded",
"timestamp": "2026-08-13T07:24:11.380Z",
"deliveryId": "6b1f0c2a-9d3e-4f58-a71b-2c8d4e5f6a7b",
"organizationId": "afb808d4-0000-0000-0000-000000000000", "signalsource": "publicsignal_auto",
"signal_type": "funding",
"signal_date": "2026-08-12",
"signal_title": "MarginEdge Raises $80M in Series D and Debt Funding",
"signal_detail": "MarginEdge Raises $80M in Series D and Debt Funding",
"whyitmatters": "MarginEdge just raised $80M: new budget and pressure to grow pipeline.",
"signalsourceurl": "https://www.finsmes.com/...",
"amount": "$80M",
"investors": "Union Square Ventures, Osage Venture Partners",
"contact_id": "70b72a7b-...",
"contact_name": "Adam Booth",
"contact_email": "adam@marginedge.com",
"contactlinkedinurl": "https://linkedin.com/in/adamjbooth",
"job_title": "Director of Revenue Operations",
"company": "MarginEdge",
"company_domain": "marginedge.com",
"company_website": "https://marginedge.com",
"company_linkedin": "https://www.linkedin.com/company/marginedge",
"company_industry": "Food and Beverage",
"companyemployeerange": "1001-5000",
"companyrevenuerange": "Above $50M",
"list_id": "62ec6931-...",
"list_name": "Funding signals — Q3",
"member_id": "68c4d70a-...",
"added_at": "2026-08-13T07:24:11.380Z",
"discovery_metadata": { "...": "the stamp ACA's own email writer reads" }
}
Empty fields are omitted, not null. A custom-signal lead carries less than a public-signal one, so guard with {{ $json.amount ?? '' }} rather than assuming a key exists.
discoverymetadata is passed through verbatim. It is the object ACA’s sequence generator reads to resolve {{aisignal_*}} tokens, so you can hand it straight back to Contact > Update on a contact you create elsewhere.
> Until 0.5.0 this event had its own node, ACA Signal Trigger. n8n allows one trigger node per service, so it was folded into ACA Trigger — same event, same filter, same output shape. If you used the old node, select signalleadadded here and everything downstream keeps working.
Example workflow
Reply to warm leads and tag them, then hand the conversation to a human.
{
"name": "ACA - handle replies",
"nodes": [
{
"parameters": { "events": ["lead_replied"] },
"type": "n8n-nodes-aca.acaTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "trigger",
"name": "ACA Trigger",
"credentials": { "acaApi": { "id": "1", "name": "ACA account" } }
},
{
"parameters": {
"resource": "eventAction",
"action": "add_tag",
"targetBy": "contact_id",
"target": "={{ $json.data.contact_id }}",
"tagName": "replied"
},
"type": "n8n-nodes-aca.aca",
"typeVersion": 1,
"position": [220, 0],
"id": "tag",
"name": "Tag as replied",
"credentials": { "acaApi": { "id": "1", "name": "ACA account" } }
},
{
"parameters": {
"resource": "eventAction",
"action": "add_note",
"targetBy": "contact_id",
"target": "={{ $json.data.contact_id }}",
"noteContent": "={{ 'Replied to ' + $('ACA Trigger').item.json.data.sequence_name }}"
},
"type": "n8n-nodes-aca.aca",
"typeVersion": 1,
"position": [440, 0],
"id": "note",
"name": "Log the reply",
"credentials": { "acaApi": { "id": "1", "name": "ACA account" } }
}
],
"connections": {
"ACA Trigger": { "main": [[{ "node": "Tag as replied", "type": "main", "index": 0 }]] },
"Tag as replied": { "main": [[{ "node": "Log the reply", "type": "main", "index": 0 }]] }
}
}
Things worth knowing
These are ACA API behaviours that will bite you if you assume otherwise. The node surfaces each of them as inline help too.
Creating contacts skips duplicates silently. With Dedupe on Email on (the default), an existing email is counted in skipped and its ID is not in contact_ids. A “create then add to list” chain therefore drops everyone who already existed. Look contacts up by email afterwards if you need every ID.
Updating a contact merges custom fields, but replaces tags. Contact > Update
exposes every updatable field individually, and its Custom Fields collection
picks keys from a dropdown of your organisation’s definitions – values are merged,
so fields you do not list keep what they had, and an empty value removes one.
tags is the exception: it still replaces the whole list, so use
Event Action > Add Tag to add one without disturbing the rest.
Smart lists have no stored members. A smart list is a saved filter that ACA
evaluates when you open it, so Lead List > Get Contacts returns nothing for one
however full it looks in the app. The list picker labels them. Use a manual,
imported or pool-built list.
Lead lists cannot be deleted over the API. Use Archive instead. This is deliberate on ACA’s side – a deleted list would orphan enrollments that reference it.
Enrolling nothing is a normal outcome. Enrollment > Create returns counts, not a success flag: enrolled, skipped, noEmail, suppressed and skippedActiveElsewhere. A lead already active in another sequence is skipped by default, because ACA will not let two sequences email the same person at once. Branch on enrolled, not on the absence of an error.
List membership events are per contact. listmemberadded fires once per
row, so a bulk add or a pool build produces one delivery per contact. It costs
nothing when nobody subscribes, but subscribe on an organisation that builds
large lists and expect proportionally large bursts. Set the lead list filter and
the burst is confined to the lists you actually picked.
contactupdated is noisy. It fires on every row change, so a bulk edit or an enrichment run produces a large burst. If you want meaningful transitions, subscribe to stagechanged or score_changed.
Pagination is cursor-based and unordered by date. There is no offset and no total count – the underlying tables run to millions of rows. Return All walks the cursors for you.
A failed Event Action returns 400. An unknown contact, an enrollment that matched nothing, a missing field – all 400 with a message saying which. Only a completed action returns 200.
Compatibility
Tested against n8n 1.x with n8n-workflow 2.x. No runtime dependencies.
Building workflows with Claude
This repo ships a Claude skill that teaches
Claude how to build n8n workflows with this node – the exact parameter names, the
patterns worth copying, and the ACA behaviours that decide whether a workflow is
correct rather than merely valid.
Copy the folder into your skills directory:
cp -r skills/aca-n8n-workflows ~/.claude/skills/
Then ask for what you want – “build me an n8n workflow that tags every lead who
replies and notifies Slack” – and it will use the right operations, know that
enrolling nothing is a normal outcome, and not reach for a smart list expecting
members.