Back to Nodes

Servicely

Last updated Aug 28, 2026

n8n community node for the Servicely ITSM/ESM JSON REST API (v1)

240 Weekly Downloads
597 Monthly Downloads

Included Nodes

Servicely
Servicely Trigger
The attached Servicely AI Agent Tool Auth credential decides what a caller has to present: Basic, Header or JWT authentication.

Description

n8n-nodes-servicely

![NPM Version](https://www.npmjs.com/package/@synergyconsulting/n8n-nodes-servicely)
![License: MIT](https://opensource.org/licenses/MIT)

An n8n community node for the Servicely ITSM/ESM platform. It talks to the Servicely JSON REST API (v1) so your workflows can read and write records on any table (Incident, Request, User, Group, …), manage file attachments, run a full-text Global Search, raise requests against the service catalog, and call instance controllers directly. A companion Servicely Trigger node starts workflows on a schedule by dequeuing async-queue messages or polling a table by filter, and the Servicely AI Agent Tool pair exposes a workflow as a tool the service desk agent can call.

Installation · Credentials · Operations · Trigger · AI Agent Tool · Examples · Compatibility · Development

Installation

In n8n (community nodes)

1. Go to Settings → Community Nodes → Install.
2. Enter @synergyconsulting/n8n-nodes-servicely and confirm.
3. The Servicely node and Servicely API credential become available after n8n restarts.

> Community nodes require self-hosted n8n, or n8n Cloud with verified community nodes enabled.

Local development (run it in n8n)

With a global n8n installed (npm install -g n8n):

cd n8n-nodes-servicely
npm install
npm run dev

npm run dev builds the node, links it into n8n’s custom-nodes directory
(~/.n8n/custom/node_modules/@synergyconsulting/n8n-nodes-servicely), and starts n8n at
. n8n loads nodes at startup, so after changing code,
stop it (Ctrl+C) and re-run npm run dev to pick up the changes.

Credentials

Create a Servicely API credential:

| Field | Notes |
|——-|——-|
| Instance URL | Base URL of your instance, e.g. https://your-instance.servicely.ai. No trailing slash, no /v1. |
| Authentication Method | Bearer Token, Basic Auth, or HMAC. |
| API Token | For Bearer/HMAC. The full System API Token (prefix + secret). Manage under Administration → Integration → System API Tokens. |
| Username / Password | For Basic Auth. |
| Shared Secret | For HMAC — used to sign each request with HMAC-SHA256. |

Secrets are stored encrypted by n8n and are never written into workflow data.

The Servicely AI Agent Tool Auth API credential is separate and points the other way: it describes what an incoming tool call has to present (Basic Auth, Header Auth, or a JWT bearer token verified with a shared secret or a PEM public key). See AI Agent Tool.

Operations

Object (any table)

| Operation | Method | Notes |
|———–|——–|——-|
| Get | GET /v1/{Table}/{id} | Fetch a single record. |
| Get Many | GET /v1/{Table} | List records, with Return All (auto-paginates) or a Limit. |
| Create | POST /v1/{Table} | Create from Fields to Set. |
| Update | PATCH /v1/{Table}/{id} | Patch the given fields. |
| Delete | DELETE /v1/{Table}/{id} | Delete by id. |

Choosing the table and its fields:

  • TableFrom List reads the instance’s TableDefinition registry and stores each row’s Table value: the API table name that goes into /v1/{Table}. Or switch to By Name for a table name / expression.
  • Record ID (Get / Update / Delete) — a plain id field, typed or from an expression ({{ $json.id }}). There is no picker: the table is arbitrary, so an id coming from an upstream node wires straight through instead of being hunted for in a list.
  • Field Name (in Filters and Fields to Set) — From List shows the selected table’s fields: the table name is resolved to its TableDefinition id, then FieldDefinition rows with that TableId are listed. Or switch to By Name for anything the registry cannot list: a dot-walked relation (Requestor.Email), or a field on a table that is itself set by expression.
  • Selecting data (Get / Get Many → Options):

    The field entries are dropdowns loaded from the selected table’s FieldDefinition rows — no typing field names by hand. Change the Table and the lists reload. A table set by an expression cannot be resolved at design time, so the dropdown comes up empty; switch the parameter to an expression there and pass a comma-separated list (which is also what workflows saved before these dropdowns keep sending).

  • Fields — multi-select of the fields to return. Empty means the API default (every field).
  • Display Value Fields — multi-select of reference fields to return as { value, displayValue }.
  • Relation Fields — still a typed comma-separated list (Requestor.Name,Requestor.Manager.Name): the registry holds one table’s own fields, while a relation path walks through other tables.
  • Sort Field (single-select of the same field list) / Sort Descending.
  • Filtering (Get Many):

  • Filters — a simple builder: pick a field (from the list or by name), an operator, and a value; conditions are combined with AND.
  • – Operators: =, !=, startswith, contains, doesnotcontain, isempty, isnotempty, in, notIn, <, >, <=, >=, between.
    - For in / notIn / between, enter a comma-separated list. isempty / isnotempty take no value.

  • Query (JSON) (in Options) — for OR/NOR or nested logic. When set, it takes precedence over the simple Filters. Example:
  •   { "and": [
          { "fieldName": "Priority", "operator": "in", "value": ["1", "2"] },
          { "fieldName": "Closed", "operator": "=", "value": false }
      ] }
      

    Attachment

    | Operation | Notes |
    |-----------|-------|
    | Upload | Reads a binary field from the input item and attaches it to a parent record. |
    | Download | Fetches an attachment by id and emits it as binary output. |
    | List | Lists the attachments on a parent record (filterable by related field). |

    Parent Record uses Servicely's {recordId}:{tableName} format (e.g. abc123:Incident), built for you from the Parent Table + Parent Record ID fields.

    Attachment ID (Download) is a plain id field like Record ID — typed or from an expression ({{ $json.id }}), with no picker, since the id normally comes from an upstream List or Upload.

    Global Search

    Full-text search over one table through the instance's Global Search controller (POST {instanceUrl}/controller/GlobalSearch) rather than through a /v1 query.

    | Operation | Body | Notes |
    |-----------|------|-------|
    | Search | { requesttype: "search", tableclass, text } | Search one table for the given text. |
    | Batch Search | { requesttype: "batchsearch", table_class, text, limit } | Same request, capped at Limit (default 50). |

  • TableFrom List posts {"requesttype": "searchconfig"} to the same controller and lists the tables it is configured to search, taking each entry's table as both label and value (its id is not used). That value is sent as table_class. Or switch to By Name for a table class / expression.
  • Search Text — the text to match.
  • The response is emitted like any other controller answer: a list of hits fans out to one item per hit, an object becomes one item, a scalar is wrapped as { data: ... }, and an empty response yields { success: true }.

    Queue

    Acknowledge a message dequeued by the Servicely Trigger back to the Async Integration controller (POST {instanceUrl}/controller/AsyncIntegration) — the counterpart of the Node-RED Success/Failure reply nodes.

    | Operation | Sends | Notes |
    |-----------|-------|-------|
    | Reply Success | action: success, status: ok | Mark the message as processed successfully. |
    | Reply Failure | action: fail, status: error | Mark the message as failed. |

  • Reply To — the message id, defaulting to ={{ $json._servicely.replyTo }} (emitted by the trigger), so it auto-wires when the trigger feeds this node.
  • Payload — the response payload returned to Servicely (defaults to the incoming item's {{ $json }}).
  • Service Catalog

    Raise a request against a published catalog item through the instance's Service Catalog controller. It is a single POST: the node does not read the catalog item, does not create the request record and does not write Answer rows — where the record goes and how each answer is stored is the instance's to decide, and the controller owns all of it.

    | Operation | Method | Notes |
    |-----------|--------|-------|
    | Create Request | POST {instanceUrl}/controller/ServiceCatalog | Sends the catalog item and the answers keyed by question record id. |

    {
      "catalogItem": "60fded6e8ca511eda18bbe07b09d6f655",
      "answers": {
        "60fded6e8ca511eda18bbe07b09d6f62": "5659d8d08b4c11f1b8fa2eb7ca4b3cdd",
        "98fa827f8ca511eda18bbe07b09d6f62": "someone@example.com",
        "7e0028718ca711eda18bbe07b09d6f62": "free text",
        "4e24c3ee8ca811eda18bbe07b09d6f62": "123132"
      }
    }
    
  • Catalog ItemFrom List shows CatalogItem records, storing each record's id and labelling it by Name. This is the one record picker that does not prefer Number: a catalog item is published, and recognised, under its name, and a list of numbers would be unusable. An item carrying no name is labelled with its id rather than dropped, so it is never silently missing from the list. Or switch to By ID for an id / expression.
  • Questions — a resource mapper rendering the questions the selected item asks as a form. Its schema comes from every Question row whose Parent is {catalogItemId}:CatalogItem (fetched in full, not one page — a question left off the form is an answer the request cannot supply), and each field's id is the question's record id, which is what answers is keyed by.
  • - Questions are ordered by the Question row's Order field (read as a number or as the string a JSON field holds it in); a row without a usable one sorts last, and ties fall back to the label so the form is stable across reloads.
    - A question is marked required only when Mandatory or Required is a real boolean true.
    - The label is Name [Datatype]. The datatype stays visible even though it also drives the input widget, because Servicely's datatypes are finer than n8n's field types — a reference and a free-text answer are both edited as strings, so dropping it would hide the difference between questions that look identical.
    - Datatype maps to the widget: boolean/checkbox → boolean, date/datetime → dateTime, time → time, url → url, int/integer/number/decimal/float → number, and anything else → a plain text box. A wrong widget would block an answer the instance would have accepted; a text box never does.
    - The mapper depends on catalogItemId.value, so picking another item reloads the form instead of leaving the previous item's questions on screen. With no item selected it shows Select a catalog item to load its questions without calling the API; an item that genuinely asks nothing shows This catalog item asks no questions; and a read that fails empties the form with Could not load the questions for this catalog item rather than failing the node editor.

  • An answer left blank is dropped rather than sent as an empty string, so an optional question that was skipped stays unanswered. Nothing answered at all still posts answers: {}.
  • A stale item id, a missing mandatory answer, or an answer the instance rejects comes back as the controller's own error. There is one write, so there is never a partly built request to clean up.

    The response is emitted like any other controller answer (see Controller). With the reference controller below that is one item per request:

    { "Success": true, "TargetTable": "Incident", "TargetRecordID": "a1b2c3..." }
    

    so {{ $json.TargetRecordID }} chains into the next node.

    #### Instance setup: the ServiceCatalog controller

    This operation needs a controller named ServiceCatalog on the instance — Servicely does not ship one. Create it in the Controller workspace with the name ServiceCatalog (the name is the URL segment, so it must match exactly) and give it a script that accepts catalogItem and answers. The reference implementation:

    const requestedForField = "RequestedFor"
    const requestedField = "Requested"
    const questionsSourceField = 'QuestionsSource'
    const catalogItemRecord = TableProtected("CatalogItem", catalogItem)
    if (!catalogItemRecord) {
        answer = {
            isError: true,
            error: "CatalogItem is not founded"
        };
        return;
    }
    const targetTable = catalogItemRecord.Table();
    const nameField = catalogItemRecord.NameField();
    const targetTableRecord = Table("Incident").newRecord()
    if (!targetTableRecord.hasField(questionsSourceField)) {
        answer = {
            isError: true,
            error: "Target table don't has questionsSource Field"
        };
        return;
    }
    targetTableRecord.RequestedFor()
    targetTableRecord.setFieldValue(nameField, catalogItemRecord.Name);
    if (targetTableRecord.hasField(requestedForField)) {
        targetTableRecord.setFieldValue(requestedForField, user.getID());
    }
    if (targetTableRecord.hasField(requestedForField)) {
        targetTableRecord.setFieldValue(requestedForField, user.getID());
    }
    targetTableRecord.create();
    Object.keys(answers).forEach((key) => {
         let answer = Table("Answer")
            .newRecord();
        answer.RelatedRecord(targetTableRecord.getID() + ":" + targetTable);
        answer.Question(key);
        answer.Answer(answers[key]);

    answer.create(); }); const checks = TableChecks .NoTableChecksNoSystemFieldsNoChecksNoEvents .withEvaluateFieldValues( true ); targetTableRecord.tableChecks(checks);

    targetTableRecord.setFieldValue( questionsSourceField, catalogItem );

    targetTableRecord.update();

    answer = { "Success" : true, "TargetTable" : targetTable, "TargetRecordID" : targetTableRecord.getID() }

    What it does, and what the node relies on:

  • catalogItem and answers arrive as the controller's own variables — the two keys the node posts, nothing else.
  • The request record is created first, then one Answer row per entry of answers, related to it as {recordId}:{tableName} and pointing at the question by its record id. That id is the mapper field's id, which is why the Questions form is keyed by Question row ids rather than by names.
  • The record's name field (catalogItemRecord.NameField()) is set to the catalog item's name, and RequestedFor to the calling user, so the request is attributed to whoever the credential authenticates as.
  • QuestionsSource is written last, under NoTableChecksNoSystemFieldsNoChecksNoEvents, so setting it does not re-fire the table's events. The target table must have a QuestionsSource field — the script refuses the call with Target table don't has questionsSource Field if it does not.
  • It answers { Success, TargetTable, TargetRecordID }, which the node emits as-is. On a bad item id it answers { isError: true, error: ... } with HTTP 200, so branch on isError (or on Success) rather than relying on the node to throw.
  • Three things to know before copying it as-is:

  • The target table is hardcoded to Incident. catalogItemRecord.Table() is read into targetTable and used for the Answer rows' RelatedRecord, but the record itself is created with Table("Incident"). If your catalog items point at more than one table, change that line to Table(targetTable) — otherwise every request lands in Incident while its answers claim to belong to the item's own table.
  • The RequestedFor block appears twice and the second run is a no-op; targetTableRecord.RequestedFor() (no argument) and the unused requestedField are inert. Harmless, and safe to delete.
  • Nothing here validates that a mandatory question was answered. The Questions form marks required questions in n8n, but a workflow that fills the mapper from an expression can still post an incomplete answers — add the check to the script if that matters.
  • Controller

    Invoke any controller registered on the instance directly — the escape hatch for instance-specific controllers the typed resources above do not cover.

    | Operation | Method | Notes |
    |-----------|--------|-------|
    | Invoke | POST {instanceUrl}/controller/{ControllerName} | Posts a raw JSON body. Controller endpoints sit at the instance root, not under /v1. |

    This operation was called Call and stored as operation: "call", which is still accepted and runs exactly the same request — a workflow that names the old value does not have to be edited. n8n saves only the parameters that differ from their default, so a workflow built in the UI never held the value at all; one created through the API or imported as JSON does.

  • ControllerFrom List shows SystemController records, storing each record's Name (the URL segment) and labelling it with Label / Title / Description when present; or enter a controller name / expression manually.
  • Body (JSON) — the request body, passed through untouched. It must be a JSON object; an expression may supply an object directly.
  • The response is emitted as-is: an array fans out to one item per entry, an object becomes one item, a scalar is wrapped as { data: ... }, and an empty response yields { success: true }.

    > Note on upload: attachment upload is implemented as a direct POST /v1/Attachment with a base64 Data field. This path is not explicitly documented for inbound REST — validate it against your instance. If your instance rejects it, front the upload with a small custom controller accepting { mimeType, fileName, base64String, parentRecord, relatedField }. Field names and the ParentRecord format are confirmed by the docs.

    Trigger

    The Servicely Trigger is a polling node — n8n adds a Poll Times schedule and calls it on that interval. Each poll that finds work starts one execution, emitting one item per message/record.

    Trigger On → Async Queue Message — claims messages from a Servicely Async Integration queue (POST {instanceUrl}/controller/AsyncIntegration, action: "dequeue"), the same mechanism as the Node-RED Queue node.

  • Queue — the queue to claim from. From List shows ActionProviderInstance records with ConnectionType = async_integration, using each record's ConnectionString as the value; or enter a ConnectionString / expression manually.
  • Action Name — the subject identifying which messages to claim. From List shows Action records for the selected queue's provider instance (ProviderInstance = the chosen instance's id), using each Action's Command as the value; or enter a command / expression manually.
  • Messages Per Poll — max messages claimed per poll (default 10).
  • Each emitted item is the message payload (a JSON object payload becomes the item's json directly; anything else is wrapped under payload). Reply metadata is attached under _servicely (replyTo, queue, subject).
  • > Dequeue is at-least-once: a claimed message may be redelivered until it is acknowledged. Close the loop with the Queue → Reply Success / Reply Failure operation (see Queue), which uses the carried _servicely.replyTo id.

    Trigger On → Object (Table Records) — polls a table and emits records matching a filter, reusing the same Filters / Query (JSON) / selector / sort surface as Object → Get Many.

  • Table, Limit, Filters, and Options (Fields / Display Value Fields / Relation Fields / Query (JSON) / Sort).
  • Each poll returns the current matches (up to Limit); it does not track a cursor, so pair a narrowing filter with an action that advances state (e.g. set a "processed" flag) to avoid re-emitting the same records.
  • Request Options (both nodes)

  • Timeout (ms) — per-request timeout (default 30000).
  • Max Retries — retries on rate limits (429), server errors (5xx), and network failures, with exponential backoff + jitter (default 3; 0 disables). Retry-After is honored. Client errors (400/401/404/422) are never retried.
  • AI Agent Tool

    The Servicely AI Agent Tool Trigger turns a workflow into a tool the Servicely service desk agent can call. It declares the tool, serves it on an HTTP POST endpoint, and validates the call before the workflow runs. The answer goes back through the Servicely node, under the AI Agent Tool resource.

    | | what it does |
    | --- | --- |
    | Servicely AI Agent Tool Trigger | declares and serves the tool |
    | ServicelyAI Agent ToolSend Response | answers the call |

    The two jobs cannot live on one node: n8n opens a webhook for every instance of a node type that declares one, so a single node would open a dead endpoint for every response node in the workflow. They are the trigger and the action node — and not two nodes of their own — because n8n verification allows a package one regular node, with a trigger for the same service alongside it. The responder was its own servicelyAiAgentTool node until 1.2.0; see Compatibility.

    Servicely AI Agent Tool (trigger)

    The tool is exported under the node's name (as [n8n] ), so the node asks for no name of its own — rename the node on the canvas and the next activation renames the tool. One node is one tool, so a workflow can declare several by holding several AI Agent Tool nodes; name them after what they do, since two nodes both left at the default "Servicely AI Agent Tool" register two tools the agent cannot tell apart.

  • Prompt — what the tool does and when to call it. Exported with the tool, so the agent reads it when deciding.
  • Path — the path the tool listens on, appended to the webhook base URL (the full path is served as given, without an internal webhook id).
  • Parameters — the tool's arguments. Each row is a Param Name, a Param Type (String, Number, Integer, Boolean; defaults to String), a Param Required toggle (on by default) and a Param Description that is exported with the tool.
  • - A required argument the call leaves out is rejected. Turn the toggle off and the call runs without it — the workflow then sees the argument absent from parameters rather than present as null, so its meaning is the workflow's to decide.
    - Only the presence check turns off. An argument that is sent is held to its declared type whether or not it had to be sent: optional means the agent may leave it out, not that it may get it wrong.
    - Param Required is checked here only. The tool is exported with the argument either way, so the agent is not told an argument is optional — it may keep sending it; this node simply stops rejecting the calls that do not.
    - A parameter saved before the toggle existed reads as required, which is how every declared parameter behaved until then.

  • IsLiveRun — a boolean parameter every tool carries on top of the declared ones, exported last so it never reorders them. Its description tells the agent to send true unless the user explicitly asked for a test run, so a workflow can tell a live call from a rehearsal without each tool defining its own flag. It is the one parameter that is not validated: a call that omits it, or sends something other than a boolean, still runs — the value is passed on as it came, and its absence is left for the workflow to interpret rather than assumed to mean anything. It is also not treated as unknown when Allow Unknown Parameters is off. Declaring a row named IsLiveRun replaces it — type, description and position then come from that row, and it is validated like any other parameter.
  • Respond — when and how the agent is answered, following n8n's own Webhook node: Using Servicely Node (default), Immediately, or When Last Node Finishes. The node never writes the response itself; it declares the mode, the status code and the data on its webhook, and n8n sends it.
  • - Using Servicely Node — the request stays open until a Servicely node set to AI Agent Tool → Send Response runs, however long the workflow takes. A branch that never reaches one never answers.
    - Immediately — answers as soon as this node validated the call, with { "success": true, "message": "Workflow was started" } unless Options → Response Data or No Response Body says otherwise.
    - When Last Node Finishes — answers with the last executed node's data, shaped by Response Data: First Entry JSON (default), All Entries, or No Response Body.

  • Tool Timeout (Seconds) — how long the service desk waits for this tool to answer before giving up on the call, exported with the tool as TimeoutSeconds. Default 60, and shown under the two modes that make the agent wait; Immediately has already answered, so it does not ask. It is the only deadline in play: n8n keeps the request open for as long as the workflow runs, so this bounds the agent's wait, not the workflow's — a workflow that overruns it keeps going, it just answers into a call nobody is waiting for any more.
  • The Respond setting and the wiring have to agree, and a call that would go unanswered is refused with a 500 rather than left hanging: Using Servicely Node with no responder downstream fails with No Servicely node set to "AI Agent Tool" found in the workflow, and a responder under either other mode fails with "Unused …" — n8n has already replied by the time that node runs, so its answer would go nowhere. The check runs before the caller is even authenticated. Only a Servicely node whose Resource is AI Agent Tool counts; the ones doing the tool's actual work are ignored.
  • On Validation Error — respond 400 with the errors (default), or run the workflow anyway and pass them on in json.validation.
  • Options → Allow Unknown Parameters (default on), Coerce Types (default off, converts e.g. the string "12" to 12 before validating), AI Agent Names or IDs, AI Assistant Names or IDs, Role Names or IDs, Mutates Ticket, Production Restricted, and Execution Script.
  • Options → the response onesResponse Code (default 200), Response Headers, Response Data (a fixed body for Immediately), No Response Body, and, for When Last Node Finishes returning First Entry JSON, Response Content-Type and Response Property Name (answer with one property of the item instead of the whole JSON). Each shows only under the modes it applies to, and none appears under Using Servicely Node — the responder carries its own status, body and headers.
  • Options → AI Agent Names or IDs / AI Assistant Names or IDs — who the tool is exported to: multi-selects loaded from the instance's SystemAIAgent and SystemAIAssistant tables, each entry labelled by its Name and stored by its record id. Reading them needs the Servicely API credential; a table that cannot be answered for leaves that list empty, which is also how an instance without one reads. Activating the workflow links the tool to exactly what each selects (see below). The two are independent — selecting agents does not touch the assistants — and an option you never add is left alone entirely: that table is not even read, since a workflow that says nothing about assistants is not asking for its tool to be taken out of them. Adding an option and then emptying it is a statement, and unlinks the tool from everything in that table.
  • Options → Role Names or IDs — the roles the tool is given: a multi-select loaded from the instance's Role table, each entry labelled by its Name and stored by its record id, written to the tool's own Roles array. Unlike the agents and assistants, this is a plain field of the tool record rather than a link held by the other side, so there is nothing to reconcile — the selection is written as it stands.
  • Options → Mutates Ticket — whether calling the tool changes something. Turn it on for tools that create, update or delete records, send messages, trigger external automations, or otherwise cause side effects.
  • Options → Production Restricted — whether the tool is kept out of production environments. When on, it cannot be selected, executed or modified on a production system — for keeping an AI from, say, changing the schema of a live instance.
  • These three share one rule: an option you never add is left alone, an option you add is written as it stands. Adding Roles and selecting nothing empties the tool's roles; adding a toggle and leaving it off writes false. Never adding them leaves whatever the service desk holds untouched, so a flag set there by hand survives every activation. (n8n keeps a collection option a workflow added even when its value equals the option's default, which is what makes "added and off" a different thing from "absent".)

  • Options → Execution Script — the script the service desk runs for this tool, exported with it. Every tool gets one whether or not you add this option: the default script posts the call's parameters to this workflow and answers with what it returns, since a tool registered without a script would do nothing. Add the option only to replace it — leaving the box blank falls back to the default, so writing a no-op means writing one.
  • Every @@WEBHOOK_URL@@ in the script is replaced with this tool's webhook URL when the workflow is activated, so a script can name its own endpoint without being edited per instance. Details:
    - It is always the production URL, even when a "Listen for test event" is what registered the tool. The script chooses its endpoint at call time — the default one rewrites /webhook/ to /webhook-test/ when IsLiveRun is false — so handing it a test URL would leave it deriving a test URL from a test URL.
    - A bare @@WEBHOOKURL@@ comes out in single quotes; one the script already wrapped in ', " or ` ` keeps the quotes it was written with, so post('@@WEBHOOKURL@@') gives post('https://…') rather than a doubled pair.
    - A script that uses
    @@WEBHOOK_URL@@ when n8n cannot resolve the URL fails the activation rather than registering a broken script.
    -
    @@URL@@, what the placeholder was called before it said what it stood for, is still resolved the same way, so a script written against it keeps working. New scripts should use @@WEBHOOK_URL@@.

    None of the fields take an expression. The node has no input, and its values are read on activation — when there is no execution to resolve one against.

    The emitted item carries body, parameters (the declared arguments the call actually sent, after coercion), headers, query, params, validation, and — with a JWT credential — the verified jwt payload.

    The node takes two credentials, both required: the Servicely API one (it backs the AI Agents, AI Assistants and Roles lists, and the registration below) and a Servicely AI Agent Tool Auth API one deciding what a caller has to present. The JWT algorithm is taken from the credential, not from the token, so a caller cannot downgrade the signature.

    Registration in the service desk

    Activating the workflow registers it as a tool; deactivating removes it. n8n drives this through the webhook lifecycle hooks, so it happens on activate/deactivate — never on a manual execution.

  • On activate the registration is upserted against a SystemAITool record whose Key is the n8n node id: a new record gets Key = the node id, Name = [n8n] , Active = true, SelectionPrompt = the node's Prompt, TimeoutSeconds = the node's Tool Timeout (how long the service desk waits for a call to be answered — n8n itself holds the request open for as long as the workflow runs; an unusable value, such as an emptied box, registers as the default 60), ExecutionScript = the node's script with its URL placeholder resolved, and a Description naming where it came from — the node, the workflow, and a link to that workflow (Created by the "Create Incident" node of the n8n workflow "My Workflow" (https://n8n.example.com/workflow/abc123)) — while an existing one is patched with the same fields minus the Key. That keeps activation idempotent: a record left behind by a deactivation that could not reach the instance is updated instead of failing on a duplicate Key.
  • Three more fields are written only when the node mentions them: Roles, MutatesTicket and ProductionRestricted, from the options of the same names. Each is a field a service desk may also set by hand, so an option the workflow never added is left as it is rather than overwritten on every activation — while an option that is there is sent as it stands, an empty selection and an off toggle included.
  • The parameters follow the tool. Each declared parameter becomes a SystemAIToolParameter row with Name, Type, Description, Parent = the tool's id, and Order counting from 10 in steps of 10. Param Required is deliberately not among them — it says what this node's webhook rejects, and the parameter table is not asked to carry a column it may not have. On a re-registration the rows are read back by Parent and reconciled by name: a new parameter is created, one whose type, description or position moved is patched, a row whose parameter the node no longer declares is deleted, and a row that already matches is left alone. Because Order comes from the declared order, reordering the collection in n8n reorders the tool's arguments. The API answers a query that matched nothing with a 404 rather than an empty list, so a 404 on the read back is taken as "no rows yet" — which is what every first registration sees; a 404 on the first write is the parameter table not being there under that name, and says so instead.
  • The agents and assistants follow their selections. The link lives on the holder — a SystemAIAgent or SystemAIAssistant holds a Tools array — so activation reads each selected table once (a registry whose option was never added is skipped without a request) and reconciles it against that table's selection: a record you selected that does not hold the tool is patched to include it, one that holds it but is no longer selected is patched to drop it, and a record already in the right state is not written at all. Linking and unlinking run as two tasks over two disjoint sets, per table, alongside the parameter sync. Entries are compared by id whether the instance stores them as bare ids, as references, or as a serialised list; a 404 on one record's write is that record having gone and is logged rather than failing the activation, and a table that is not there at all reads as "nothing to link".
  • On deactivate the tool comes out of every agent's and assistant's Tools first, so none is left pointing at a record that is about to go — a failure there is logged and the delete goes ahead anyway. The record is then looked up by that same Key and deleted if it is there. Nothing is cached between the hooks — the Key is the tool's whole identity, so a restart or a record edited in the service desk changes nothing about what the hooks find.
  • "Listen for test event" registers the tool like an activation does, so it can be exercised from the service desk while you are still building the workflow. Stopping the listen deliberately does not remove the registration: n8n tears a test webhook down exactly the way it deregisters a production one, and removing it there would deregister a workflow that is active at the same time.
  • Nothing left to remove is not treated as a failure, and removal never throws: n8n clears a workflow's webhooks on the way into activation as well, so a throw there would block activating the workflow too. Real failures (an expired token, a 500) are logged at error level instead.
  • The Key is the node id, so each Servicely AI Agent Tool node owns exactly one tool record and a second node in the same workflow registers a second tool of its own. The id is n8n's, and it survives everything a workflow can do to a node except deleting it — renaming it, moving it, editing its parameters — so a tool keeps its registration, and its links to agents and assistants, across all of those. Delete the node and the next activation deregisters its tool.
  • Renaming the node renames the tool (Name is always sent), and its links survive that too, since they hang off the record rather than its name.
  • A node with no id — a workflow assembled outside the editor — fails with "The node has no id yet".
  • AI Agent Tool → Send Response

    A resource of the Servicely node, not a node of its own. It is the only resource that asks for no Servicely API credential and offers no Request Options: it answers the request a tool call is still holding open, and never talks to the instance.

  • OperationSend Response, the one operation.
  • Respond WithSuccess (status + data) or Error (status + message + optional JSON details, e.g. {{ $json.validation.errors }}).
  • Data — all incoming items, the first incoming item, a JSON body you write, or no data.
  • Options → Envelope (default on) wraps the body in { "success": true, "data": … } / { "success": false, "error": … }; Message adds a note to a success; Response Headers adds headers.
  • 204 and 304 are sent without a body. Items pass through unchanged, so the workflow can carry on after responding. One request gets one answer however many items reach the node: the response is built from the whole batch and sent once.
  • The trigger must have Respond set to Using Servicely Node; under either other mode it refuses the call outright, rather than leaving this node with an answer nobody is waiting for. n8n holds the request open until this node runs, however long that takes — the trigger's Tool Timeout is what decides how long the service desk waits for it.
  • Examples

    Get all open incidents, sorted newest first

    1. Servicely → Object → Get Many, Table Incident.
    2. Return All on.
    3. Filters:
    State != Closed.
    4. Options → Sort Field
    CreatedOn, Sort Descending on.

    Create an incident

    1. Servicely → Object → Create, Table Incident.
    2. Fields to Set:
    ShortDescription = Printer offline, Priority = 2.

    Attach a file fetched over HTTP

    1. HTTP Request node → downloads a file into the binary field data.
    2. Servicely → Attachment → Upload, Parent Table
    Incident, Parent Record ID = the incident id, Input Binary Field data.

    Process an async queue

    1. Servicely Trigger → Async Queue Message, Queue = your queue, Action Name = the subject, Poll Times every minute.
    2. Downstream nodes handle each message (
    json is the payload; json._servicely.replyTo identifies it).
    3. Servicely → Queue → Reply Success to acknowledge (or Reply Failure on an error branch). Reply To defaults to
    ={{ $json._servicely.replyTo }}.

    Expose a workflow as an agent tool

    1. Servicely AI Agent Tool Trigger, renamed on the canvas to Create Incident (the tool registers as [n8n] Create Incident), Prompt "Creates an incident for a user and returns its number", Path create-incident, AI Agent Names or IDs = the service desk agent.
    2. Parameters:
    shortDescription (String, required, "What is wrong"), priority (Integer, Param Required off, "1 highest to 4 lowest — omit for the default").
    3. Options: Mutates Ticket on, since the call creates a record.
    4. Servicely → Object → Create, Table
    Incident, fields taken from ={{ $json.parameters.shortDescription }} and ={{ $json.parameters.priority }} — the second is absent when the agent omits it, so give it a default downstream.
    5. Servicely → AI Agent Tool → Send Response, Respond With Success, Data First Incoming Item.

    Compatibility

  • The AI Agent Tool responder became a resource of the Servicely node in 1.2.0. n8n verification allows a package one regular node plus a trigger for the same service, and this package had two regular nodes. The servicelyAiAgentTool node is removed; what it did is now Servicely → AI Agent Tool → Send Response, with the same fields under the same names. The trigger is untouched — same servicelyAiAgentToolTrigger type, same parameters, same registered tool — so an active workflow keeps its tool registration and its endpoint; only the node that answers has to be replaced. To migrate a workflow: open it, delete the Servicely AI Agent Tool Response node, add a Servicely node in its place with Resource AI Agent Tool and Operation Send Response, copy the Respond With / Data / Options values across, and reconnect it. The trigger's Respond option that was called Using Servicely AI Agent Tool Response Node is now Using Servicely Node; its stored value is unchanged, so a saved workflow keeps the mode it had and needs no edit there. Until the responder is replaced the trigger refuses calls with No Servicely node set to "AI Agent Tool" found in the workflow rather than leaving the agent waiting.
  • AI Agent Tool node types changed in 0.7.0 so the pair could become one entry in the nodes panel: the trigger became servicelyAiAgentToolTrigger and the node that answered became servicelyAiAgentTool (which 1.2.0 then replaced with the resource above). The types published before that — servicelyAiTool (trigger) and servicelyAiToolResponse — were kept registered and hidden through the 0.7.x line and are removed as of 0.8.0. A workflow still on them loads with unrecognised nodes: its endpoint stops answering and its tool stays registered in the service desk until the workflow is opened, the two nodes replaced with the current pair, and the workflow re-activated (the tool re-registers under the new node's id, leaving the old record to be deleted by hand). Replace both halves together — a current trigger no longer recognises an old response node.
  • Service Catalog → Create Request needs a ServiceCatalog controller on the instance. Servicely does not ship one; the operation posts to POST {instanceUrl}/controller/ServiceCatalog and the controller owns everything about where the request record goes and how each answer is stored. See Instance setup: the ServiceCatalog controller for a reference script. Without it the node fails with the instance's own "no such controller" error.
  • Requires an n8n version supporting community nodes (n8nNodesApiVersion: 1).
  • Servicely REST API v1. Record creation returns HTTP 200 (not 201).
  • Minimum Servicely versions for optional features:
  • - Batch API (POST /v1/_batch): 1.4.2-release.40+.
    - Bearer token as URL parameter and
    moveAttachments: 1.10+.

    Development

    npm install
    npm run build       # tsc → dist/ (+ copies the node icon)
    npm run build:watch # tsc --watch (recompile on change)
    npm run dev         # build + link into ~/.n8n/custom + start n8n
    npm run typecheck   # tsc --noEmit
    npm run lint        # eslint, including n8n's own node standards
    npm test            # vitest run
    npm run test:coverage
    

    Tests stub helpers.httpRequestWithAuthentication rather than hitting a live instance, so the suite runs offline. Coverage is enforced at ≥80% per file (statements, branches, functions, lines).

    npm run lint runs n8n's own node standards (eslint-plugin-n8n-nodes-base, the presets a community node is measured against) alongside the package's rules, so the check a submission faces is the check that runs here. Two rules are turned off in eslint.config.mjs, each with its reason: both expect the docs slug a credential in n8n's own repository uses, where this package holds the URL that actually helps a reader.

    Publishing

    .github/workflows/publish.yml publishes to npm on a version change in package.json, from CI only — n8n requires every community node to be published by a GitHub action carrying a provenance statement, which a local npm publish cannot produce. The job lints, typechecks and tests before it publishes, then runs n8n's @n8n/scan-community-package against the published version. It needs one repository secret, NPM_TOKEN (an npm automation token with publish rights on the @synergyconsulting scope).

    Architecture

    The layout follows n8n's actions/ router convention: one folder per resource,
    one file per operation.

    nodes/Servicely/
      Servicely.node.ts            # thin shell: description + router
      ServicelyTrigger.node.ts     # polling trigger (no resource/operation pair)
      actions/
        router.ts                  # resolves the operation, owns the item loop
        versionDescription.ts      # INodeTypeDescription, composed from the resources
        node.type.ts               # resource → operations union the router narrows on
        common.descriptions.ts     # property fragments used by more than one operation
        object/
          index.ts                 # operation selector + shared Table field
          create.operation.ts      # each file: its own properties + execute(index)
          delete.operation.ts
          get.operation.ts
          getAll.operation.ts
          update.operation.ts
        attachment/
          index.ts
          download.operation.ts
          list.operation.ts
          upload.operation.ts
        globalSearch/
          index.ts
          request.ts               # the Table + Search Text pair and the POST both share
          search.operation.ts
          batchSearch.operation.ts
        queue/
          index.ts
          reply.ts                 # the call both reply operations share
          replyFailure.operation.ts
          replySuccess.operation.ts
        serviceCatalog/
          index.ts
          createRequest.operation.ts # one POST to /controller/ServiceCatalog
        controller/
          index.ts
          invoke.operation.ts      # raw POST to /controller/{ControllerName}
        aiAgentTool/
          index.ts
          sendResponse.operation.ts # answers the request a tool call holds open
      GenericFunctions.ts          # API request helpers + query builders
      SearchFunctions.ts           # listSearch pickers + loadOptions loaders
                                   #   (fields, AI agents, AI assistants, roles)
                                   #   + the resourceMapping schema for catalog questions
      constants.ts
      types.ts

    nodes/ServicelyAITool/ ServicelyAIToolTrigger.node.ts # webhook trigger: declares + serves the tool presentation.ts # the names, codex and docs the two halves share response.ts # the Respond modes, as n8n's Webhook node does them registration.ts # webhookMethods: registers the tool + its parameters identity.ts # the registered tool's Key, Name and Description parameters.ts # reads the declared parameters, for both sides validation.ts # the four parameter types, coercion, body check authentication.ts # Basic / Header / JWT, per the attached credential jwt.ts # JWS verification (HS/RS/PS/ES), algorithm pinned

  • Each *.operation.ts exports description (its properties, scoped with updateDisplayOptions) and execute(this, index) handling one item.
  • router.ts owns the item loop, continueOnFail, and error wrapping, so operations carry no boilerplate.
  • node.type.ts makes the resource/operation pairing a compile-time union — an unregistered operation fails to build rather than at runtime.
  • The package registers one regular node and two triggers, which is what n8n verification allows: Servicely acts, ServicelyTrigger and ServicelyAIToolTrigger start workflows. Anything that would otherwise want a regular node of its own becomes a resource of Servicely instead — which is how aiAgentTool got there. The class name has to match the file's base name, since n8n's loader derives the one from the other, so a node file and its class are renamed together or not at all.
  • nodes/ServicelyAITool/ sits outside the actions/ convention because the trigger has no resource/operation pair. It keeps only its description and the webhook handler, and everything else lives in the helper modules next to it, which is what makes them directly unit-testable. Two of those are about the feature's two halves agreeing with each other: presentation.ts holds the names and codex the trigger shows and the strings that identify the responder, and response.ts holds the Respond modes the trigger declares and the aiAgentTool resource fulfils — including the check that a workflow set to answer from a Servicely node actually has one.
  • credentials/ServicelyApi.credentials.ts — its authenticate resolves the instance URL into baseURL and signs every request (Basic / Bearer / HMAC), so no node code reads credentials.
  • SearchFunctions.ts — every From List picker is paginated. Servicely's list endpoints are offset-based, so each picker page returns n8n's paginationToken (the next page number) whenever the API filled the page; n8n asks for the next one as the user scrolls. Because the API has no text-search parameter, the typed filter is applied per page — a page emptied by filtering still hands back its token, so matches further in the table are not stranded. The Table and Field Name pickers are the exception: neither is searchable, so n8n loads each registry in one go and filters client-side, and the paging happens internally (bounded, since it runs at design time).
  • Adding an operation means: add .operation.ts, register it in the
    resource's
    index.ts (export + selector option), and add it to node.type.ts.

    Passing n8n's verification scan

    npx @n8n/scan-community-package @ is what n8n runs on a published
    version, and it lints the attested source — the repository at the commit named in
    the package's npm provenance, tests included — with
    allowInlineConfig: false. An
    eslint-disable comment therefore counts for nothing there: a rule has to be
    satisfied by the code, not exempted.
    npm run lint:scan runs the same lint the same
    way locally, so a finding shows up before a release rather than after one.

    npm run lint (n8n's own CLI) does honour inline comments, so the two disagree by
    design —
    lint:scan is the stricter of the pair and the one that gates verification.

    > The scanner's published versions pin typescript@7 against a @typescript-eslint
    > that requires
    <6.1, so npx aborts on ERESOLVE before running it. Until that is
    > fixed upstream,
    lint:scan` is the practical way to get the same answer.

    Resources

  • Servicely REST API docs
  • n8n community nodes

License

MIT