MCP server
Connect AI tools to Flowtly through the Model Context Protocol at mcp.flowtly.eu.
Connect
claude mcp add --transport http flowtly https://mcp.flowtly.eu/mcp
On this page
Agreements
Tools
| agreements_get | Get one employment agreement by id — type, variant, the dateFrom/dateTo window, hoursPerWeek, and the derived `calculable`, `active` and `status`. agreements_list supplies the id. Requires ROLE_AGREEMENTS_MANAGER or ROLE_MEETING_MANAGER. Read-only. |
| agreements_list | List employment agreements — filter by employee (IRI), isActive, type or variant. THE way to answer "why does people_list say this person is inactive": each row carries `calculable` and `active`, and a person is active exactly when they hold one that is both. Also the place to read the agreement `type` codes this org actually uses before calling agreements_create, since an org can add its own. Requires ROLE_AGREEMENTS_MANAGER or ROLE_MEETING_MANAGER. Read-only. |
| agreements_create | Create an employment agreement for a person. THIS IS THE STEP THAT MAKES SOMEBODY ACTIVE: people_create only makes the record, and a person with no agreement reports isActive false forever — a bulk import therefore lands 100% inactive until this is run for each of them. TWO THINGS BOTH HAVE TO BE TRUE or they stay inactive with no error: the `type` must be a CALCULABLE one (built-ins "agreement", "annex", "termination" are; "list-of-intent" and "work-experience" are not), and the dateFrom/dateTo window must cover today (pass dateTo null for an ongoing contract rather than a far-future date). `employee` is an IRI — /people/<id> from people_list. Types are org-extendable, so run agreements_list on somebody already active to see the codes this org really uses. Requires ROLE_AGREEMENTS_MANAGER. Write. |
| agreements_update | Change an existing employment agreement — the way an agreement is ENDED, because the backend exposes no delete on this resource: set `dateTo` to the last day it covers and the person stops being active from then, with the record and its history intact. That is the correct move for a payroll-adjacent row; there is no way to make one disappear and there should not be. Also the way to correct a wrong `type`, `variant` or `positionName` in place rather than stacking a second agreement on the person — TWO agreements do not cancel out, the calculable one keeps them active, so "add a correct one alongside" silently leaves the wrong one in force. `amount`, `amountType` and `billingType` are accepted but the API never returns them, so you cannot read back what you wrote. Requires ROLE_AGREEMENTS_MANAGER. Write. |
Agreement Types
Tools
| agreementTypes_get | Get one contract type by id — its name or translationKey, `calculable`, `isActive`, `position` and `builtIn`. The id IS the code, so this reads back a type by the same string an agreement stores in `type`. Use it to confirm a type persisted after agreementTypes_create, and to check `calculable` before putting anyone on it. Requires ROLE_USER. Read-only. |
| agreementTypes_list | List the contract types THIS org can put on an agreement — the values behind `Ludzie > <person> > Umowy > Edytuj umowę`. Read it before agreements_create or agreements_import, because the list is per-tenant: five built-ins ship ("agreement", "annex", "termination", "list-of-intent", "work-experience") and an org can add its own, so a `type` that is valid in one org 422s in another. THE ID IS THE CODE — the `id` on each row is exactly the string `agreements_create` wants in `type`, not a numeric key to look up. `calculable` is the field that decides whether holding this type makes somebody ACTIVE and counts them into the resourcing bench, holiday accrual and the cost base; a non-calculable type leaves them inactive with no error anywhere, which is intended for a type like "list-of-intent" and a silent bug if you picked it by accident. `builtIn` rows carry a translationKey and a null name; custom rows carry a name rendered verbatim and a null translationKey. Requires ROLE_USER. Read-only. |
| agreementTypes_create | Add a contract type to THIS org's list, so an agreement can be recorded against something the five built-ins do not cover — "Umowa zlecenie", "Kontrakt B2B", "Użytkownik funkcyjny". This is configuration, not a code change: the list is a per-tenant table, and a custom type needs no translation entry because its `name` renders verbatim in all seven locales. DO NOT SEND `id`: the code is slugged from the name server-side with diacritics folded ("Użytkownik funkcyjny" becomes "uzytkownik-funkcyjny"), and passing an id is refused with 422 "Update is not allowed for this operation". Post the name and read the assigned code back off the response. `calculable` DEFAULTS TO FALSE AND IS SILENT: it decides who counts as employed — the resourcing bench, holiday accrual, the cost and budget base — so a type meant for people who should NOT accrue leave or occupy an FTE is correct at false, and a type meant for real employment MUST set it true or everyone on it reports inactive with no error. Nothing will tell you which you got. `position` orders the dropdown; `isActive` defaults true. There is no update or delete over MCP on purpose — `agreement.type` stores this row's id as a bare string with no foreign key, so renaming or removing a type orphans every agreement pointing at it. Requires ROLE_AGREEMENTS_MANAGER. Write. |
Allocations
Tools
| allocations_get | One allocation by id — a single person's booking on a project, with its dates and percentage. allocations_list finds the id; this reads the full record. An allocation with no employee is an OPEN role (unfilled demand), not a booking. Needs the resourcing module. Read-only. |
| allocations_list | List resourcing allocations — date-ranged assignments of a position on a project to an employee (or to nobody yet, an open role). No filters; page with cursor. Each item carries employeeId/employeeName and projectId/projectName already resolved (null employeeId means an open role); positionId is bare — resolve its name via positions_list. source distinguishes sheet-imported rows from ones created directly in Flowtly. Use this to reconcile a resourcing sheet import: read back what landed and compare against what was submitted. |
Asset Bookings
Tools
| assetBookings_get | Get one asset booking by id — the asset, its holder, the dates, and whether it has been cancelled. Read-only. |
| assetBookings_list | List asset bookings — who or what currently holds each asset, which is the assignment the Assets screen shows and the only place an asset-to-person link actually lives. Each row carries the asset, the holder (`relationName` employee | project plus `relationId`), start/end dates and, once released, `cancelReason` and `cancelledAt`. Filter by `property` to see one asset's history, or by `employee` to see everything one person holds — that second one is what to run before someone leaves. Note `employee` here is the NUMERIC id, not the /people IRI that assetBookings_create takes. Add `exists.cancelledAt: false` to see only what is still held; without it the list includes released bookings too. Read-only. |
| assetBookings_create | Assign an asset to a person or a project. `property` is the asset IRI (/assets/{id}) and is required. Name the holder ONE of three ways: `relation` with a single IRI (/people/{id} for a person, /projects/{id} for a project), or `relationName` (employee | project) plus `relationId`, or the `employee` / `project` IRI field directly. Exactly one holder must resolve — naming neither is refused with "Employee or Project must be set." and naming both with "Employee and Project cannot be set at the same time." TWO THINGS THAT ARE NOT IN THE SCHEMA AND WILL 422 YOU: the asset must already be reservable (`bookingAllowed: true` — set it with assets_update), a business rule enforced for EVERY caller including a manager, refused with "This asset is not reservable."; and the asset's own `bookingType` (minutes | days | single-days | permanently) is what makes sense of `duration` / `endDate` — a space dedicated to one person indefinitely is `permanently` with a `startDate` and no end. Concurrent bookings on one asset are serialised server-side, so an overlap is refused rather than double-booked. Requires ROLE_PROPERTY_BOOKINGS_MANAGER to book on someone else's behalf. Write. |
| assetBookings_update | Update an existing asset booking — its dates, duration, billing amount/currency, or metered-consumption share. `relationName` and `relationId` are required by the payload, so send the holder the booking already has unless you are deliberately moving it. To end an assignment use assetBookings_cancel, not an endDate in the past. Requires ROLE_PROPERTY_BOOKINGS_MANAGER. Write. |
| assetBookings_cancel | Release an asset — the way an assignment ends, and the closest thing this resource has to a delete (there is no delete operation). Takes the booking id and a `cancelReason` of 3-255 characters; the booking is kept and stamped with `cancelledAt` so the history survives, and the asset becomes free for the next holder. This is the call to make when an employee leaves: assetBookings_list filtered by `employee` finds what they hold, and this releases each one. Requires ROLE_PROPERTY_BOOKINGS_MANAGER. Write. |
Asset Meter Readings
Tools
| assetMeterReadings_get | Get one meter reading by id — its meter, date and value. Read-only. |
| assetMeterReadings_list | List meter readings — the dated values recorded against an asset meter, the raw data the metered-billing split reads. Each row carries the meter, date and value. Use it to read a meter's history: a value that never moves across periods (a stuck or shared meter) bills zero, and a meter with no recent rows is one nobody is reading. Read-only. |
Asset Meters
Tools
| assetMeters_get | Get one asset meter by id — the asset it sits on, utility type, unit and external/QR identifier, with its readings. Read-only. |
| assetMeters_list | List the org's asset meters — the utility/media counters attached to assets (electricity, water, gas, heat). Each carries the asset it sits on, its utility type and unit, and its readings. Filter by `property` (the asset it belongs to) and `utilityType`. Use it to resolve the meter id that readings take, and to spot meters that read zero, are stuck on one value, or sit on a shared/collective meter. Read-only. |
| assetMeters_update | Update an asset meter — its label, utility type, unit, or active state. Use it to retire a meter from metering (e.g. a utility now billed straight from the invoice) without deleting its reading history. Requires ROLE_PROPERTIES_MANAGER. Write. |
Assets
Tools
| assets_get | Get one asset by id — name, status, category (attributeSet), parent, assetCode, serial number, purchase and warranty dates, location and booking settings. Read-only. |
| assets_list | List the org's assets — the register of physical things it owns or sells, from laptops and desks to apartments, parking spaces and storage units. Filter by status (in-stock | damaged | sold), attributeSet (the category the Assets list groups by), bookingAllowed, or a partial name or serialNumber; order by name, status, serialNumber, boughtAt or warrantyTo. NOT PAGINATED — the whole set comes back in one response, so a large register is one big payload rather than a first page. Use it to resolve the asset id that asset bookings and asset documents take. Read-only. |
| assets_import | Load MANY assets in one call, keyed on `assetCode` — the tool for bringing an inventory across from another system, where assets_create would be one round trip per record. Rows reconcile against the org: an unknown assetCode creates, a known one updates in place, a matching row is skipped, so re-running changes nothing and a half-finished run is safe to repeat. `parentAssetCode` nests a row under another BY ITS CODE, resolved against the org and against earlier rows of the same batch; a parent that never resolves fails that row rather than silently orphaning it. THREE FIELDS MAKE THE RECORD READABLE rather than a bare name: `attributeSetName` is the category the UI shows as Typ zasobu and the list groups by, `locationName` is where the thing physically is, and `attributes` is a {name: value} map for area, floor, price and anything else the source carries. All three are resolved BY NAME — the category, the location, the attribute definitions and their bindings are found or created for you, so a caller never handles one of those IRIs, and names are matched case-insensitively so "Mieszkanie" and "mieszkanie " cannot split the list in two. `attributes` needs a category to hang off, and a value that parses as a number creates a number attribute, decided the first time the name appears. An attribute that fails to write does NOT fail its asset. PASS dryRun:true FIRST on a real inventory load — it reports would-create / would-update / would-skip per row and creates nothing at all, categories and locations included. Max 1000 rows. Requires ROLE_PROPERTIES_MANAGER. Write. |
| assets_create | Create an asset (name + status + bookingType required; status = in-stock | damaged | sold, bookingType = minutes | days | single-days | permanently). bookingType is required even when the asset is never booked — pass "permanently" for something that is not lent out, and leave bookingAllowed false. Two fields carry the structure: `parent` nests an asset under another (a unit under a building, a monitor under a desk), and `attributeSet` sets the category the Assets list groups by, which is also where custom attributes such as area or floor live. `assetCode` is a UNIQUE cross-system handle — use it to hold the id this asset has in the system of record it was imported from, so a re-import updates rather than duplicates. Requires ROLE_PROPERTIES_MANAGER. Write. |
| assets_update | Update an asset by id — name, status, category, parent, assetCode, serial number, dates, location or booking settings. This is how an asset moves from in-stock to sold. Note the status vocabulary is in-stock | damaged | sold and has NO reserved state, so a hold has to be modelled some other way. Requires ROLE_PROPERTIES_MANAGER. Write. |
Attribute Entity Values
Tools
| attributeEntityValues_list | List attribute VALUES — what a specific asset, project, budget or client actually holds for a bound attribute. Each row carries the attribute, the value, and `relationId` naming the entity it belongs to. Read-only. |
| attributeEntityValues_create | Set an attribute value on one entity (attribute + value required). `relation` IS AN IRI — "/properties/7", not the word "property": the backend resolves it and derives the relation name from the resource class, so passing a bare name throws. (`relationId` takes a plain id and still works, but it is deprecated in favour of the IRI.) The attribute must already be BOUND to that entity's category or the value is stored and never displayed. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
| attributeEntityValues_update | Change one attribute value in place, by its id. Use this rather than creating a second value for the same (entity, attribute) pair — nothing enforces uniqueness, so a duplicate is accepted and the UI shows one of them. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
| attributeEntityValues_delete | Remove an attribute value from an entity. The definition and the binding survive; only this entity's value goes. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
Attributes
Tools
| attributes_get | Get one attribute definition by id — name, type, whether it is required or multiple, default value and format pattern. Read-only. |
| attributes_list | List attribute DEFINITIONS — the named fields (area, floor, price) that categories bind and assets carry values for. Each has a type: number | string | date | state | period. Read-only. |
| attributes_create | Create an attribute definition (name + type required; type is number | string | date | state | period). THE TYPE IS THE DECISION: it is shared by every entity carrying this attribute, so a field created as `string` cannot later total or sort as a number without every existing value being rewritten. Decide it against the values you actually have, not the first one you see. A definition on its own does nothing — bind it to a category with attributeSetAttributes_create, or it never appears anywhere. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
| attributes_update | Update an attribute definition — name, type, required, multiple, default or format. Changing `type` on a definition that already has values is the risky one: existing values are not converted. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
Attribute Set Attributes
Tools
| attributeSetAttributes_list | List the bindings between categories and attribute definitions — which fields appear on which category. Read-only. |
| attributeSetAttributes_create | Bind an attribute definition to a category (attributeSet + attribute, both IRIs). THIS IS WHAT MAKES AN ATTRIBUTE APPEAR: without the binding a value can be written successfully against an entity and will never show in the UI — a failure with no symptom. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
| attributeSetAttributes_delete | Unbind an attribute from a category. The definition and any values survive; they simply stop being shown for that category, which makes this look like data loss when it is not. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
Attribute Sets
Tools
| attributeSets_get | Get one attribute set by id — its name, relationName, icon, and the attributes bound to it. Read-only. |
| attributeSets_list | List the org's attribute sets — the CATEGORIES an asset, project, budget or client is filed under. Filter by relationName: "property" for asset categories (what the UI calls Typ zasobu and the Assets list groups by), plus "project", "budget" and "client". Reach for this before creating one: a category duplicated by spelling or casing silently splits the list it groups, and nothing in the UI explains why. Read-only. |
| attributeSets_create | Create a category (name + relationName required; relationName is one of property | project | budget | client, and for an asset category it is the plain string "property" — NOT an IRI). Optional icon from a fixed list (room, parking, building, office, local, desk, monitor and so on) which the UI shows beside the category. LIST FIRST: names are not unique, so a second "Mieszkanie" is accepted and quietly splits the Assets list in two. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
| attributeSets_update | Rename a category, change its icon, or move it to another relationName. This is how a category created with a typo gets fixed rather than duplicated. Requires ROLE_ATTRIBUTES_MANAGER. Write. |
Bank Accounts
Tools
| bankAccounts_get | Get one bank account by id — name, currency, bank, and the format its statements are imported in. |
| bankAccounts_list | List the org's bank accounts. Filter by bank, or set hidden to include archived ones. Use it to resolve the bankAccount id that transactions_list filters on. |
| bankAccounts_create | Create a bank account (type, name, currency, defaultImportFormat required). Write. |
| bankAccounts_update | Update a bank account by id. Write. |
Banks
Tools
| banks_get | Get one bank by id — the institution, not an account held with it. Use bankAccounts_get for the account. |
| banks_list | List the banks the org's accounts are held with. Hidden banks are INCLUDED by default — pass hidden=false for the pickers' view, or hidden=true to find the retired ones. Use it to resolve the bank id that bankAccounts_list filters on and that bankAccounts_create needs. |
| banks_create | Create a bank — the institution a bank account belongs to, not the account itself (that is bankAccounts_create). Write. |
| banks_update | Update a bank by id. This is also how a bank is hidden and un-hidden: set `hidden` true to retire one from the pickers without deleting it, false to bring it back. There is no separate archive tool because the API has no archive action for a bank — the flag is the mechanism. Write. |
Budgets
Tools
| budgets_employeePnl | P&L per employee for a budget — what each person's time earned against what they cost. Needs ROLE_BUDGETS_VIEWER. Read-only. |
| budgets_get | Get one budget by id — its period, scope and settings. Needs ROLE_BUDGETS_VIEWER. Read-only. |
| budgets_list | List the org's budgets — the periods against which income and cost are planned and compared. Use it to resolve the budget id that every pnl tool takes. Needs ROLE_BUDGETS_VIEWER. Read-only. |
| budgets_pnlByTags | P&L for a budget, broken down BY TAG — income, costsByTag, costsByProject and netByTag over the budget's periods. The tag axis is what makes this readable for a business whose costs are not naturally per-project: tag the documents, and the split follows. Carries displayPricePerSqm when the org has enabled price-per-sqm and named an area attribute, which is what turns this into a per-square-metre view for a property developer. Needs ROLE_BUDGETS_VIEWER. Read-only. |
| budgets_pnlByTagsDrilldown | The documents behind one cell of budgets_pnlByTags. Reach for it when a tag total looks wrong — it names the transactions making up the number instead of leaving you to guess. Needs ROLE_BUDGETS_VIEWER. Read-only. |
Clients
Tools
| clients_get | Get one client by id — name, country, currency, tax id and status. |
| clients_list | List clients (the org's customers). Filter by status, or by externalPaymentCustomerId to find the client behind a payment-provider id. Use it to resolve the client id that invoices_list, deals_list, projects_list and contracts_list all filter on. |
| clients_import | Load MANY clients in one call, keyed on `externalRef` — the tool for bringing a customer or buyer list across from another system, where clients_create would be one round trip per person. Rows reconcile against the org: an unknown externalRef creates, a known one updates in place, a matching row is skipped, so re-running changes nothing. The ref is stored as `externalPaymentCustomerId`, the only external-reference column a client has, and `clients_list` filters on it. DO NOT match clients by name instead — a buyer list is full of shared surnames and joint purchases. Each result carries `counterpartyId`, which is what contracts_import and contracts_create need. Two traps the schema cannot express: a `tin` is REJECTED without a `tinCountry`, and a contact row needs an e-mail, so a phone number alone cannot create one. PASS dryRun:true FIRST on a real onboarding load. Max 500 rows. Requires ROLE_CLIENTS_MANAGER. Write. |
| clients_create | Create a new client record (name, country, currency, status, tinType required). Write. |
| clients_update | Update a client record by id. Write. |
Config Keys
Tools
| configKeys_catalog | List every organization config key the backend recognises, with its type and allowed values. This is the catalog of what is configurable — read it before configs_get or configs_update rather than guessing a key name. Permission is enforced per key by the backend, so a key appearing here does not guarantee the connected user may write it. |
Configs
Tools
| configs_get | Read one organization config value by id, where the id is a key from configKeys_catalog (e.g. organization-logo-url, organization-icon-url). |
| configs_update | Update an organization config value by id (type + name required; permission is enforced per config key by the backend). Write. |
Contracts
Tools
| contracts_get | Get one contract by id — parties, direction, value, cyclic terms and dates. |
| contracts_list | List contracts. Filter by direction — the stored values are "out" (we sell / issue) and "in" (we buy / receive), plus "unknown" — a real, filterable state rather than an error. A contract created by uploading a document starts as "unknown" and stays there until extraction or a person settles it, so omit the filter to get all three: "in" and "out" queried separately DO NOT add up to the whole set (flowtly-mcp#130). NOT "outgoing"/"incoming": those match nothing and come back as an empty list rather than an error. Also filters on counterparty, project, cyclic, name or tags. Use it to resolve the contract id that contracts_paymentScheduleLines reads and that deals_win can link a won deal to. |
| contracts_paymentScheduleLines | List a contract's payment schedule — the instalments it is expected to be invoiced or paid in. Pass contractId from contracts_list. This is the plan, not the actuals: compare it against transactions_list to see what has really been paid. Each line's amount is in MINOR UNITS — grosze, not złote: "530000" is 5 300,00, so divide by 100 before reporting a figure to anyone. |
| contracts_import | Load MANY contracts in one call, keyed on `name` — the agreement number. Unlike a client or an asset, a contract has NO external-reference column, so the name IS the idempotency key; a batch containing the same name twice is REFUSED WHOLE rather than updating one contract twice, because a duplicate number means the source is wrong. `counterpartyExternalRef` resolves the buyer through the same ref clients_import was given, so the two compose: import the clients, then the contracts, without ever handling a numeric counterparty id — a ref matching no client fails that row rather than creating a partyless contract. `direction` is "out" (we sell) or "in" (we buy); the column has no server-side constraint, so a wrong word is stored and the contract then matches no filter anywhere. PASS dryRun:true FIRST. Max 500 rows. Requires ROLE_CONTRACTS_MANAGER. Write. |
| contracts_create | Create a contract. Write. |
| contracts_update | Update a contract by id. Write. |
| contracts_delete | Delete a contract by id. Write. |
Cost Groups
Tools
| costGroups_list | List cost groups / cost centres — the buckets that costs, suppliers and incoming invoices are filed under. Use it to resolve the costGroup id that suppliers_create requires and that incoming-invoice suggestions propose. |
| costGroups_create | Create a cost group / cost center (name + type required). Write. |
| costGroups_update | Update a cost group / cost center's name or type by id. Write. |
Counterparties
Tools
| counterparties_get | Get one counterparty by id. |
| counterparties_list | List counterparties — every party the org transacts with. The supplier and client flags say which side(s) a counterparty plays, and one record can be both. This is the party on a bank transaction, so it is what incoming invoices and transactions are matched against. Filter by type, supplier, client, cyclic or budgetNeutral. |
CRM Notes
Tools
| crmNotes_get | Get one CRM note by id. |
| crmNotes_list | List notes written on leads and deals. Filter by lead or deal to read the running commentary on one record. |
| crmNotes_create | Add a note to a lead or a deal (body + exactly one of lead/deal). Author is the connected user. Write. |
| crmNotes_update | Update a CRM note’s body by id. Write. |
| crmNotes_delete | Delete a CRM note by id. Write. |
Deal Lost Reasons
Tools
| dealLostReasons_get | Get one deal lost-reason by id. |
| dealLostReasons_list | List the reasons a deal can be marked lost, in order. deals_lose requires a lostReasonId from here. |
Deals
Tools
| deals_get | Get one deal by id — title, client, stage, amount, owner, contact, expected and actual close dates. |
| deals_list | List deals/opportunities — the sales pipeline. Filter by status (open / won / lost), stage, owner, client, lead, or by expectedCloseDate / closedAt ranges. Amounts are minor units with an explicit currency; do not assume the org's default. |
| deals_create | Create a deal/opportunity. Required: title, stage (from stages_list), and an ANCHOR — at least one of client or lead. A deal with neither is refused with 422 “A deal must reference a client or a lead.”, so anchor a prospect you have no customer record for to its lead (`/leads/<id>` from leads_list) rather than inventing a client; pass client (`/clients/<id>` from clients_list) once there is one. Setting both is allowed. Optional: amountMinor, currency, expectedCloseDate, owner, contact. Creating straight into a won stage additionally requires client — a lead-only deal cannot be won. Write. |
| deals_update | Update a deal by id (title, stage, amountMinor, currency, expectedCloseDate, owner, contact, client, lead). Moving the stage is logged automatically. The anchor rule from deals_create still applies to the result, so you cannot clear the only client or lead a deal has — swap one in first. Moving a deal into a won stage requires client: attach the customer here (or run leads_convert) before winning a lead-only deal. Write. |
| deals_delete | Delete a deal by id (soft delete). Write. |
| deals_win | Mark a deal won — moves it to a won stage and stamps it closed; optional contractId links an existing contract. BACKFILLING A HISTORICAL WIN: pass optional closedAt (ISO-8601, e.g. “2026-05-07” or a full timestamp) to record the date it ACTUALLY closed. Omit it and the server stamps now, which puts an old deal in this month’s “won this month” figure — so set it whenever you are entering a deal that closed before today. It may not be in the future (422), and it MAY be earlier than the deal’s own createdAt: a deal created today and closed in May is the normal shape of a correct backfill, not an error. The deal must ALREADY reference a client: winning a lead-only deal is refused with 422 “Attach a customer before marking this deal Won.”, because there is no customer to bill. Turn the lead into one with leads_convert, or set client with deals_update, then win. Write. |
| deals_lose | Mark a deal lost — requires lostReasonId (from dealLostReasons_list); optional lostReasonNote. BACKFILLING A HISTORICAL LOSS: pass optional closedAt (ISO-8601) to record the date it ACTUALLY closed, exactly as deals_win does. Omit it and the server stamps now. It may not be in the future (422), and may be earlier than the deal’s createdAt. Write. |
| deals_reopen | Reopen a won/lost deal back to open. Write. |
Deal Stage Histories
Tools
| dealStageHistories_get | Get one deal stage-change record by id. |
| dealStageHistories_list | List a deal's stage transitions, newest first. Filter by deal. Every deals_update that moves the stage is logged here automatically, so this is how you reconstruct how long a deal sat in each stage — the deal itself only carries its current one. |
Departments
Tools
| departments_list | The org's departments, with the numeric id each one is referenced by. READ THIS BEFORE people_create or people_update: both accept a `department` IRI and there is no other way to discover a valid one. The collection is unpaginated and ordered by name, so a single call returns every department the org has. Filter by `name` (partial match) or `code` (exact). Rows carry id, name and code; `manager` is a relation and is not included in list rows — read it with people_list from the other side if you need it. Requires ROLE_EMPLOYEES_VIEWER. Read-only. |
| departments_create | Add a department, so people can be filed under it. `name` is required (up to 128 characters) and is UNIQUE across the org; `code` is optional (up to 64) and is ALSO unique — the short form an org already uses in its own spreadsheets (CEO, TECH, PROC). `manager` is an optional employee IRI from people_list. LIST FIRST AND EXPECT COLLISIONS: because both name and code are unique, re-posting a department that already exists FAILS rather than being idempotent, so an import that assumes create-per-row will stall the first time it meets a department the org already carries — typically one left over from a trial. Reconcile that row with departments_update instead of creating around it. THERE IS NO DELETE: the backend exposes no delete on a department, so a wrong name or code is corrected in place with departments_update and never removed. Requires ROLE_EMPLOYEES_MANAGER. Write. |
| departments_update | Rename a department, give it a code, or set its manager. This is the tool that makes a department import possible rather than merely convenient: `name` and `code` are both unique, so a department the org already has — the single "HR" row a proof-of-concept tends to leave behind — cannot be created again, and the real list is reached by CORRECTING that row rather than colliding with it. Only the fields you send change, so passing `code` alone leaves the name intact. `id` is the numeric id from departments_list; `manager` is an employee IRI from people_list. THERE IS NO DELETE, which makes this the whole repair story: a department created with a typo is fixed here, and one that should not exist can only be renamed, not removed. Requires ROLE_EMPLOYEES_MANAGER. Write. |
Holiday Days Limits
Tools
| holidayDaysLimits_get | One entitlement row by id — the amount, the type, the contract variant and the date it takes effect. holidayDaysLimits_list finds the id. Amounts are in SECONDS (#3763). Read-only. |
| holidayDaysLimits_list | How much leave each person is ENTITLED to, per type — not how much they have taken, which is holidays_list. Filter by employee. A person can hold several rows for one type over time, because a balance gets topped up or corrected: the row IN FORCE is the one with the latest dateFrom that has already arrived, and rows dated ahead are deliberately ignored until then. Amounts are in SECONDS (#3763) — an 8h leave day is 28800. Requires ROLE_HOLIDAYS_MANAGER. Read-only. |
| holidayDaysLimits_create | Give a person an allowance of one leave type, effective from a date. `seconds`, NOT days (#3763): an 8h day is 28800, so 21 days is 604800 and an overtime balance of 2h30 is 9000 — a figure that had nowhere to go while this was stored in whole days. `employee` and `holidayType` are IRIs (people_list and holidayTypes_list supply them); `variant` is the contract type the allowance belongs to (uop, b2b, uz, uod). To CORRECT an existing balance, add a row with a later dateFrom rather than editing the old one — the row in force is the latest one whose dateFrom has arrived, so history stays intact and a correction can be entered before it takes effect. (employee, holidayType, variant, dateFrom) is unique, so re-posting the same day replaces nothing and fails. Requires ROLE_HOLIDAYS_MANAGER. Write. |
| holidayDaysLimits_update | Fix a row that was entered wrongly — a typo in the amount, the wrong variant. Amounts are in SECONDS (#3763). This is NOT how you record a balance CHANGING over time: for that, holidayDaysLimits_create a new row with a later dateFrom, which preserves what the previous balance was and when. Editing in place rewrites history and makes the old figure unrecoverable. holidayDaysLimits_list finds the id. Requires ROLE_HOLIDAYS_MANAGER. Write. |
Holiday Requests
Tools
| holidayRequests_list | Leave REQUESTS and where they stand — pending, approved, rejected. Distinct from holidays_list, which is booked leave: a request still awaiting a decision is not yet an absence, so plan against holidays_list and use this one to see what is waiting on someone. Supplies the holidayRequestId that holidays_approve and holidays_bulkApprove take. Read-only. |
| holidayRequests_cancel | Cancel a leave request — use it to clear a request that should never be acted on, such as a row left behind by a trial, a test, or someone who has left. TWO THINGS THAT SURPRISE PEOPLE. (1) IT DOES NOT DELETE THE ROW: the backend sets status to `canceled` rather than removing the row. BUT A CANCELLED REQUEST DISAPPEARS FROM holidayRequests_list — verified on production: afterwards neither the unfiltered list nor status=canceled returns it. So you cannot read back what you cancelled and there is no undo through the MCP; be sure of the id before calling. (2) IT IS NOT THE SAME AS REJECTING. Rejecting records a decision — it writes an approval-log entry naming you and MAILS THE EMPLOYEE that their leave was refused — whereas cancelling notifies only HR, and only when `notify-hr-managers-of-leave-activity` is on for the org. For a row that was never a genuine application, cancel is the honest and quieter one. ONLY WORKS ON A PENDING (`requested`) REQUEST when you are not its owner: an accepted request has already produced a Holiday that this does not remove, so cancelling one would leave a booked absence behind a request reading `canceled`. Requires ROLE_HOLIDAYS_MANAGER for someone else's request; the requester can always cancel their own. holidayRequests_list supplies the id. Write. |
Holidays
Tools
| holidays_active | Who is off RIGHT NOW — every currently-running leave, org-wide, for everyone. This is the tool for 'who is out today', and the one to cross-check before treating resourcingBench_get's freePercent as availability, because the bench does not subtract leave. Unlike holidays_list it applies no project scoping and needs no permission beyond being signed in, so its answer covers the whole organisation. Returns each absence with its type and dates. Read-only. |
| holidays_get | One leave record by id, with its type, dates and duration. Get the id from holidays_list or holidays_active. Read-only. |
| holidays_list | Booked leave over a period — the planning view, where holidays_active answers only about today. Filter by employee, by date range, or by project. WHAT YOU SEE DEPENDS ON YOUR PERMISSIONS, and a short list is not proof nobody is off: a holidays manager or accountancy viewer gets the organisation, while a project lead or viewer MUST pass a project filter (or ask about themselves) and is refused outright without one — that refusal is a permission boundary, not an empty calendar. Read-only. |
| holidays_create | Record leave that a person is actually taking — the booked absence itself, not the entitlement (holidayDaysLimits_create) and not a pending application (holiday requests, which still need approving). What this writes is already-agreed time off, so it appears in holidays_list straight away and needs no approval step. `employee` is an IRI from people_list; `type` is a holidayTypes_list id. `dateFrom`/`dateTo` inclusive, and one call covers a whole range rather than a row per day. Two things bite: a type whose `descriptionRequired` is true (read holidayTypes_list first — `vacations` commonly is) REJECTS a create with no `description`; and `pick-up-day` is time already owed, so it does NOT consume the annual allowance the way `vacations` does — filing a day given back for a Saturday public holiday as `vacations` silently eats a day of someone's entitlement. Check holidays_list for the same person and dates before creating, because an overlap is REFUSED, not duplicated: the backend raises `validation_holiday_dates_overlap` as a 422 on `dateTo` when the range touches any day already covered by another absence for that person. The one exception is narrow — two SINGLE-DAY part-day absences on the same date, of DIFFERENT types, both `vacations` or `pick-up-day`, whose hours together fit the working day. Anything else overlapping fails. There IS a holidays_update, so retyping an absence no longer needs delete-then-create. EVERY CREATE MAILS THE EMPLOYEE, at their own company address, to say the absence was added — so loading a year of history someone already lived through arrives in their inbox row by row, and for staff who have not been invited yet it is the first they hear of Flowtly at all. LOADING A YEAR OF HISTORY? A bulk form exists — holidays_import reconciles up to 500 absences in one call, skips the ones already on file so it is safe to re-run, and defaults the mail OFF — but IT IS NOT AVAILABLE ON THIS CONNECTION: it is served only to the internal scope, so you cannot call it here and looking for it will not find it. Loop this tool, or ask your Flowtly operator to run the bulk load. Pass `notify: false` for a BACKFILL of absences that already happened; leave it alone when logging something new, because then the mail is the point. It suppresses the message only — the row, its `createdAt` and its payroll fact are written either way. Requires ROLE_HOLIDAYS_MANAGER. Write. |
| holidays_delete | Remove a booked absence outright — the row is deleted, unlike holidayRequests_cancel which only flips a request's status. Use it to clear absences that should never have counted: demo or test rows left by a trial, or ones orphaned when their employee was deleted (people_delete detaches absences rather than removing them, so they survive with an empty employee name). THIS MOVES REAL NUMBERS: a booked absence is `payrollEligible` and consumes the person's entitlement, so deleting one changes their leave balance — the point when clearing test data, and a data-loss bug when the row was genuine. No undo, no notification. Read holidays_list first and be certain the row is not real history: a description in the org's own language, or dates matching an actual absence, usually means it is. Requires ROLE_HOLIDAYS_MANAGER. Write. |
Holiday Types
Tools
| holidayTypes_list | The leave types this org uses, with the id each one is referenced by. Read it before holidayDaysLimits_create/update, which need a holidayType IRI and will otherwise be guessed at. The one that is not a holiday in the ordinary sense is `pick-up-day` — time off owed for overtime already worked (Polish *odbior nadgodzin*), which is a GRANTED balance rather than an annual entitlement. Read-only. |
| holidayTypes_create | Add a leave type the org does not yet offer — a sabbatical, unpaid childcare, a training day — so absences can be booked against it with holidays_create and an allowance granted with holidayDaysLimits_create. `name` (3–64 characters) is what people pick from when booking; `color` and `icon` are how it reads in the calendar; `reducesWorkingTime` false marks time off that does NOT lower the month's expected hours; and `descriptionRequired` true makes the type demand a reason, which holidays_create then enforces — see that tool for what it rejects. `status` defaults to `active`, so a type created without thinking about it is offered to everyone immediately. READ holidayTypes_list FIRST: types are org-wide, and THERE IS NO DELETE — a duplicate or a misspelt name can only be hidden again by setting status to inactive with holidayTypes_update, and it keeps every absence booked against it in the meantime. Requires ROLE_HOLIDAYS_MANAGER. Write. |
| holidayTypes_update | Change a leave type, and above all TURN ONE BACK ON. `status` flips between `active` and `inactive`, and an inactive type is refused by holidays_create — so recording historic leave against a type the org has since retired starts here, and this is what unblocks a leave-history import rather than sending someone into the app UI. DEACTIVATING IS NOT DELETING, and there is no delete: absences already booked keep an inactive type and still read with it in holidays_list, so inactive means only 'not offered for new bookings'. THE TRAP THAT FOLLOWS FROM THAT: reactivate `vacations` to load last year's absences, forget to set it back to `inactive`, and you have not merely finished an import — you have changed what the organisation offers today, because every employee booking leave now sees that type on the list again. Set it back in the same session you imported. `descriptionRequired` also reaches into holidays_create, which refuses a booking with no description once it is on; turning it on leaves absences already recorded alone. `id` is the string id from holidayTypes_list (`vacations`, `not-paid`), and only the fields you send change. Requires ROLE_HOLIDAYS_MANAGER. Write. |
Incoming Invoices
Tools
| incomingInvoices_get | Get one incoming (supplier) invoice or supporting document by id, with its OCR'd fields and current match state. |
| incomingInvoices_list | List incoming (supplier) invoices and supporting documents — the accountancy inbox. An incoming invoice IS a document attached to a bank transaction, so exists.transaction=false is how you find documents that are not yet matched to a payment. Filter also by status, relatedMonth, counterparty, project, tags, or hasDetectedProblems. Each document is fingerprinted as externalId 'upload_sha256:<sha256 of the bytes>' — hash a file and look for that externalId here BEFORE incomingInvoices_create, or you will file a duplicate. |
| incomingInvoices_matchCandidates | List the bank transactions that could be the payment for this incoming invoice, ranked by the backend's own matcher. Reach for it when a document has no transaction attached and you need to choose one; prefer these candidates over guessing from amounts yourself. |
| incomingInvoices_suggestions | Read Flowtly's own proposals for an incoming invoice — supplier match, cost group, matching bank transaction, duplicate warning. These are exactly the proposals a human sees in the app. Read them first, then apply one by id with incomingInvoices_applySuggestion, or take them all with acceptAllSuggestions. Pass refresh to recompute rather than serve the cached set. |
| incomingInvoices_suggestionsDebug | Explain WHY an incoming invoice's suggestions came out as they did — the matcher's scoring, for diagnosing a missing or wrong suggestion. Diagnostic only; use incomingInvoices_suggestions for normal work. |
| incomingInvoices_create | File an incoming (supplier) invoice or supporting document into accountancy — pass the bytes as base64 with a fileName and receivedAt. Flowtly OCRs it and suggests a supplier and a matching bank transaction. The file is fingerprinted as externalId 'upload_sha256:<sha256 of the bytes>': to avoid a duplicate, hash the bytes and check incomingInvoices_list for that externalId BEFORE uploading. Write. |
| incomingInvoices_applySuggestion | Accept one of Flowtly's own suggestions on an incoming invoice — the same proposals a human sees in the app (supplier match, cost group, matching bank transaction, duplicate warning). Read them first with incomingInvoices_suggestions, then apply one by its id. Prefer this over guessing: Flowtly's matcher, not the agent, decides what is plausible. Write. |
| incomingInvoices_acceptAllSuggestions | Accept every pending suggestion on an incoming invoice in one call — what a human does with the app's "accept all" button. The server applies, rebuilds, and applies again until nothing new appears: the transaction match does NOT exist until the supplier and amount are applied, so a single pass would leave the document unattached. Returns a report (what was applied, what was refused and why, and the transaction it ended up filed against). Pass dryRun to preview without writing. Never accepts supplier_create or a duplicate warning. Write. |
| incomingInvoices_checkEInvoices | Pull any new KSeF e-invoices into the org — what the app's "Sprawdź e-faktury" button does. Call this before concluding that a supplier's invoice is missing: without it you cannot tell "the supplier never sent it" from "our sync has not run yet". Returns once the fetch is queued; re-read incomingInvoices_list afterwards to see what arrived. Write. |
Initial Budget Items
Tools
| initialBudgetItems_list | List initial budget line items — the planned amounts, by tag, that contractComparison comes back against. Needs ROLE_BUDGETS_VIEWER. Read-only. |
Initial Budgets
Tools
| initialBudgets_contractComparison | PLANNED versus CONTRACTED, per tag — the initial budget's planned amounts against the sum of contract values actually signed for that project. This is the question 'have we committed more than we budgeted, and where', and it reads directly off the contracts already in the org, so importing contracts makes it answerable without any further work. Amounts are grosze; a mixed-currency project produces a notice rather than a silently wrong total. Needs ROLE_BUDGETS_VIEWER. Read-only. |
| initialBudgets_get | Get one initial budget by id, with its items. Needs ROLE_BUDGETS_VIEWER. Read-only. |
| initialBudgets_list | List initial budgets — the ORIGINAL plan for a project or investment, as opposed to the live budget it is measured against. Needs ROLE_BUDGETS_VIEWER. Read-only. |
Invoices
Tools
| invoices_get | Get one outgoing (sales) invoice by id — client, rows, totals, sale and issue dates, status. |
| invoices_list | List outgoing (sales) invoices. Filter by client, tags, search, or a saleDate range. Note that saleDate — not issue date and not creation date — is the field invoices_export filters on, so use the same one here when reconciling an export. |
| invoices_export | Start a zip export of ISSUED invoices for a period (from/to, both YYYY-MM-DD, inclusive) filtered on SALE DATE — not issue or creation date. Only ISSUED invoices are included; drafts and unsent invoices are excluded, but corrections ARE included. Optional client restricts to one client (id or IRI from clients_list). Max 200 invoices per export — if the period has more, narrow it (e.g. export one month at a time); a period with 0 issued invoices is rejected too. This call only enqueues the job (rendering a month can take minutes) — it does NOT return a download link. Poll invoices_exportStatus with the returned exportId until it reports "ready". Write. |
| invoices_exportStatus | Poll the status of a zip export started by invoices_export, by exportId. Once status is "ready", the response includes downloadUrl (a short-lived signed link — expires in 1 hour, see expiresAt), filename, and byteSize; the file's bytes are never returned through this tool. If status is "failed", failureReason explains why. |
| invoices_import | File an ALREADY-ISSUED outgoing (sales) invoice into the org — for bringing invoice history in when onboarding. The external invoice number you pass is preserved verbatim, the buyer is resolved by tax id (created if absent), and the invoice lands as issued WITHOUT rendering a PDF, emailing the client, or submitting to KSeF. Importing a number that already exists is a no-op that reports the existing invoice, so a bulk import is safe to re-run — but that guarantee holds for sequential calls only; two genuinely concurrent imports of the same number can both land. Pass expectedGrossTotal (the gross printed on the source document) and the import is rejected if it disagrees with the total computed from the rows. buyer.tin is required — the buyer is never matched by name. Use invoices_create, not this, to raise a genuine new invoice. Write. Pass dryRun:true to PREVIEW without writing — it reports would-create / would-skip and creates no invoice and no client; run a historical backfill dry first and check the counts before running it for real. |
| invoices_create | Raise a NEW outgoing (sales) invoice — the tool for billing a client for the first time. Do not confuse it with its two neighbours: invoices_import back-files an invoice that was ALREADY issued elsewhere (onboarding history), and incomingInvoices_create files a supplier's COST document. The invoice lands UNSENT: status is derived from the invoice's log rows and a fresh invoice has none, so nothing is rendered, emailed, or submitted to KSeF by this call — treat the result as a draft to review before issuing. `name` is the invoice number and is yours to choose (max 32 chars) — read invoices_list first and follow the org's existing series rather than inventing one, because nothing here allocates the next number for you. Required: name, type ("invoice"), tinType, issueDate, saleDate, dueDate. Pass `client` (IRI from clients_list) and, for a booking that reconciles later, `contract` (IRI from contracts_list) so the invoice shows up under that contract. Line items go in `invoiceRows` — net unit price, quantity and a tax rate per row; the totals are computed from the rows, not passed in. A CROSS-BORDER ROW'S RATE IS A LEGAL BASIS, NOT A NUMBER: besides the numeric rates `vatRate` takes `np I`, `np II` and `zw`, it is a free 5-character string, and nothing validates which one you send. `np I` and `np II` are DIFFERENT legal bases and land in different fields of the KSeF invoice: `np II` is P_13_9, services under art. 100 ust. 1 pkt 4 of the Polish VAT Act (the ones also reported in the VAT-UE recapitulative statement); `np I` is P_13_8, every other supply outside Poland. Which of the two a given supply is, is a tax decision: take it from the org's accountant or from the org's confirmed practice for that kind of customer, and DO NOT COPY THE RATE OFF WHATEVER `np` INVOICE THE ORG ALREADY HAS — a precedent may itself be wrong. The buyer's tax number must already be stored WITHOUT its country prefix (clients_create explains why) — this document prints tinCountry joined to tin, so a client saved as "RO40424862" prints RORO40424862 here. `bankAccount` (from bankAccounts_list) picks the account printed on the document, and `currency` defaults to the org's. Write. |
| invoices_update | Correct an outgoing (sales) invoice by id, before or after issuing. The everyday use is fixing a draft raised by invoices_create — a wrong date, a wrong row, a missing contract link — rather than deleting and re-raising it, which would burn an invoice number. Read invoices_get first: this is a PATCH over a document whose totals are derived from its rows, so replacing `invoiceRows` replaces the whole set, and an invoice that has already been sent will not un-send itself because you edited it. Write. |
Lead Activities
Tools
| leadActivities_get | Get one lead activity (outreach touch) by id. |
| leadActivities_list | List a lead's outreach touches — its activity timeline (invite sent, replies, calls, follow-ups). Filter by lead to read one prospect's history. This is the structured counterpart to crmNotes_list: activities are the typed, dated touch-log; notes are freeform commentary. |
| leadActivities_create | Log ONE outreach touch on a lead — an invite sent, an invite accepted, a message, a reply, a call, a follow-up (lead + type + occurredAt required; channel, contact, body optional). THIS is where a prospect's outreach history belongs: a crmNote is freeform commentary, an activity is the structured, filterable touch-log the prospecting queue timeline renders. Do NOT narrate touches into a note. type: invite_sent | invite_accepted | message_sent | reply_received | call | meeting | follow_up | …; channel: linkedin | email | phone | …. Write. |
| leadActivities_update | Update a logged outreach activity by id (type, channel, occurredAt, body). Write. |
| leadActivities_delete | Delete a logged outreach activity by id. Write. |
| leadActivities_byList | Every lead activity on a CAMPAIGN (a lead list), in one call — pass the list's id, IRI, or exact name. leadActivities_list filters by a single lead, so campaign-level reporting otherwise costs one call per member (302 for a list like PZFD); this resolves the list's members and reads their activities in bounded batches instead. Combine with type and occurredAt.after/.before to get the counts people actually ask for: reply rate (type=reply_received), bounce rate (type=bounced), send coverage (type=message_sent). Returns listId, listName, leadCount, and the merged activities sorted by occurredAt. An unknown list is an ERROR, not an empty result — so a mistyped name cannot read as "this campaign had no activity". Ids come from leadLists_list. Read-only. |
| leadActivities_bulkImport | Record a whole outbound wave — every message you actually sent — in ONE call, instead of one leadActivities_create per message. Pass an array; each row names its lead (leadCompanyName, matched against an EXISTING lead, or a lead IRI) plus type and occurredAt. Give every row an externalId — the stable per-message id, e.g. the Gmail message id — and the import is idempotent: re-running it, or re-running a wave that was only partly imported, reports duplicates instead of creating them. Rows without an externalId dedupe on (lead, type, occurredAt, contact), the same natural key leads_bulkImport uses, so a wave that first landed through that tool is not duplicated here. Every row gets its own outcome (created | duplicate | error), so one malformed row does not discard the rest of the batch. Does NOT create leads — use leads_bulkImport for that. ≤ 1000 rows/call. Write. |
Lead Contacts
Tools
| leadContacts_get | Get one lead contact by id. |
| leadContacts_list | List the contact people attached to leads. Filter by lead to read one prospect's contacts, or by email to find which lead a message came from. |
| leadContacts_create | Add a contact person to a lead (lead + name required; email, phone, role, linkedinUrl, isPrimary optional). A contact's LinkedIn URL belongs in linkedinUrl, NOT in a crmNote. Write. |
| leadContacts_update | Update a lead contact by id — e.g. set linkedinUrl / email / phone once you find them. Write. |
| leadContacts_delete | Delete a lead contact by id. Write. |
Lead List Memberships
Tools
| leadListMemberships_get | Get one lead-to-list membership by id. Its status and lastContactedAt are a caller-written snapshot, not live state — see leadListMemberships_list. |
| leadListMemberships_list | List which leads sit on which outbound prospecting lists. Filter by list, lead or status. CAUTION: status and lastContactedAt are a SNAPSHOT written by whoever last imported or updated the membership. They are not derived, and nothing advances them when an activity is recorded — logging a wave of 529 follow-ups moves neither field — so they can be arbitrarily far behind. To answer “when did we last touch this prospect”, read the activity log instead: leadActivities_list for one lead, leadActivities_byList for a whole campaign. leadListMemberships_syncFromActivities reports the gap and can close it. |
| leadListMemberships_create | Add a lead to an outbound list (list + lead required; status optional). Any lastContactedAt you pass is a snapshot that nothing will advance afterwards — log the touch as a lead activity as well, or it stays unqueryable. Write. |
| leadListMemberships_update | Update a lead’s membership in a list — e.g. set outreach status (contacted/replied/bounced). status and lastContactedAt are caller-maintained: what you write stands until someone writes again, and recording lead activities does NOT update them. Write. |
| leadListMemberships_delete | Remove a lead from an outbound list. Write. |
Lead Lists
Tools
| leadLists_get | Get one outbound prospecting list by id. |
| leadLists_list | List outbound prospecting lists. Use it to resolve the list id that leadListMemberships_create takes. |
| leadLists_create | Create an outbound prospecting list (name required). Write. |
| leadLists_update | Update an outbound list by id. Write. |
| leadLists_delete | Delete an outbound list by id. Write. |
Lead Lost Reasons
Tools
| leadLostReasons_get | Get one lead lost-reason by id. |
| leadLostReasons_list | List the reasons a lead can be marked lost, in order. |
Leads
Tools
| leads_dedupeCheck | Check whether a prospect is already in the CRM, using the same filters as leads_list (companyName, source, owner, …). Call this BEFORE leads_create: a duplicate lead splits the outreach history across two records, and nothing downstream will merge them for you. |
| leads_get | Get one lead by id — company, website, source, status, owner and the client it converted to, if any. |
| leads_list | List leads — prospect targets, before qualification. Filter by status, source, owner, client, companyName, or createdAt/closedAt ranges. A qualified lead becomes a Client plus an open Deal via leads_convert; until then it lives only here, not in clients_list. |
| leads_create | Create a lead (outbound/inbound prospect target; companyName, source, owner, linked client optional). A new lead is always status=open — status is not settable here, and moves only through leads_convert, leads_lose and leads_reopen. Write. |
| leads_update | Update a lead by id (company, website, source, owner, linked client, stage, doNotContact). NOT status or lostReason: those are refused by the entity and silently ignored by this endpoint, so closing a lead needs leads_lose (with a lostReasonId) and undoing that needs leads_reopen. Moving `stage` walks the funnel; it does not close the lead. Write. |
| leads_delete | Delete a lead by id (soft delete). Write. |
| leads_convert | Convert a qualified lead into a Client + one contact per lead-contact + an open Deal. Requires an existing client (the lead’s client or a clientId in the body). Write. |
| leads_lose | Close a lead as LOST — sets status=lost and stamps closedAt. REQUIRES lostReasonId, the `id` of a leadLostReasons entry (run leadLostReasons_list first; it is a picklist, so free text is refused with 422). This is the ONLY way to record a lead as lost: leads_update ignores status, and doNotContact means "never contact again", which is a different and much stronger statement than "we did not win this one". It does NOT move the lead's stage — LeadStage has no terminal flag, so the lead keeps its funnel position and leads_reopen can restore it exactly. Write. |
| leads_reopen | Undo leads_lose — sets status back to open and clears closedAt and the lost reason. The stage is untouched, so the lead resumes exactly where it was. Reach for this when a lead was closed against the wrong record or the prospect came back. Write. |
| leads_bulkImport | Import many leads in ONE call, each with its contacts, list membership and outreach activities nested — the server creates the lead then threads its id into the children, so you never juggle intermediate IRIs. Idempotent by natural keys (companyName / email / (list,lead) / (type,occurredAt,contact)): safe to re-run and to chunk (≤100 leads/call). This is the bulk path a campaign import should use instead of N leads_create calls. Write. |
Lead Stages
Tools
| leadStages_get | Get one lead stage by id. |
| leadStages_list | List the stages a lead moves through, in order. Leads have their own stage set — deals use stages_list, which is a different thing. |
Locations
Tools
| locations_get | Get one location by id — its name and office hours. Read-only. |
| locations_list | List the org's locations — the physical places assets sit, shown in the UI as Lokalizacja. Needs ROLE_LOCATIONS_MANAGER, which unusually gates the READ as well as the write. Read-only. |
| locations_create | Create a location (name required; optional officeOpenHour/officeCloseHour as seconds past midnight). Use the real address rather than a project or investment name — this is what someone standing in front of the asset needs, and the project name is already carried elsewhere. Requires ROLE_LOCATIONS_MANAGER. Write. |
| locations_update | Rename a location or change its office hours. Requires ROLE_LOCATIONS_MANAGER. Write. |
Organization Addresses
Tools
| organizationAddresses_get | Get one subscription address record by id — name, street, city, postCode, country, and the tax fields. `street` carries the building number when it was entered by hand, and does not when it came from the NIP/GUS lookup. Read-only. |
| organizationAddresses_list | List the organization's subscription address records — the address attached to the Flowtly subscription, and the source the mail-footer {{organizationAddress}} renders from. Normally exactly one row. This is NOT the invoice seller address, which lives in the organization-billing-* config keys (configs_get) and is what invoices and KSeF read; the two are maintained separately and routinely disagree. Read both before concluding which one a customer actually edited. Read-only. |
| organizationAddresses_update | Update the organization's SUBSCRIPTION address record (id required; send only the fields you are changing). THIS IS THE RECORD THE MAIL FOOTER RENDERS FROM: the footer's {{organizationAddress}} is composed as "street, postCode city" from here, NOT from the organization-billing-* config keys that invoices and KSeF use as the seller address. The two stores drift, and the footer reading this one is a known defect — so when a signature shows an address the customer swears they corrected, they corrected the billing keys and this is the record still holding the old value. `street` is a single free-text column that must carry the building number too: the NIP/GUS lookup fills only the street name and silently discards the building and flat number, which is why addresses here read "ul. Example" with no number. Write the full "ul. Example 8/12" to repair it. READ FIRST with organizationAddresses_list and compare against configs_get on organization-billing-street before writing, so you copy the customer's own maintained value rather than inventing one. Requires ROLE_BILLINGS_MANAGER. Write. |
Organizations
Tools
| organizations_get | Get an organization by id. WARNING — this does NOT tell you which organization you are connected to. An OAuth connection is pinned to exactly one org (token-bound), but this endpoint returns any org the connected USER is a member of, so a successful read here reads like confirmation you are working in that org when you may not be. To verify the tenant you are actually operating on, read tenant-scoped data instead — people_list or clients_list — and never start a bulk write on the strength of this call alone. |
People
Tools
| people_get | Get one person/employee record by id — names, emails, phone, manager, and whether they are active. |
| people_list | List people/employees. Filter by isActive, reportsTo (a manager's id), projectMembers.project, or search; page with cursor. People and employees share the same id, so this is how you resolve the employee id that work time, responsibilities, project membership and permission tools all expect. |
| people_create | Create a person/employee record (firstname + lastname required; optional companyEmail, contactEmail, contactPhone). Write. |
| people_update | Update a person/employee record by id (name, companyEmail, contactEmail, contactPhone, etc.). Write. |
| people_delete | Delete an employee/person record by id (e.g. to remove a placeholder/dummy employee). Requires ROLE_EMPLOYEES_MANAGER; the backend runs a delete processor that also detaches related records. High-impact, irreversible. Write. |
| people_invite | Give an existing person a LOGIN: creates a pending organization invitation and emails it to them, in the org's configured UI language. This is the step people_create and people_setPermissionGroups do NOT do — a person with permission groups still cannot sign in until they are invited and accept. Requires the person's email; fails if they already have a login. Onboarding order: people_create (record) -> people_invite (login) -> people_setPermissionGroups (rights). Write. |
| people_setPermissionGroups | Set (replace) a person's WHOLE permission-group set by numeric group ids (see permissionGroups_list — e.g. the "Business Owner" group grants ROLE_ADMIN): pass every group they should end up with, and [] removes all of them. Grants access; does NOT create a login or email the person — that is people_invite. THE TRAP: giving someone their FIRST group moves them onto the computed model, where roles come from groups and per-person overrides, and a role granted to them by hand outside that model disappears in the very same call — a ROLE_ADMIN handed to one person is exactly the kind this removes. It cuts the other way too: clearing their last group moves them back off it and makes those older roles reappear. The overridesAdded/overridesRemoved lists say nothing about any of this; they describe overrides and stay empty while effective access changes. So the response reports the difference between the roles the person held before this call and after it, as rolesLost and rolesGained — that is the pair to read once the call returns. rolesLost null (not []) means the snapshot taken before the write could not be read and the delta is UNKNOWN, with the reason in roleDeltaUnavailable: the group change still happened, so a null is not a clean bill of health — re-check with people_getPermissions. To give back a role that should have survived, grant it with people_setRoleOverrides. Requires ROLE_ROLES_MANAGER. Write. |
| people_setRoleOverrides | Set (replace) the roles ONE person gets on top of — or has taken away from — their permission groups. Reach for a group first (people_setPermissionGroups): groups are the intended abstraction and scale to more than one person, so use an override only where a single individual genuinely differs from every group. REPLACES both lists wholesale, so read people_getPermissions first and pass back every override they should keep; omitting a list clears it. Roles are ROLE_ constants — permissionGroups_list shows the ones this org already uses. A role in both added and removed is refused rather than guessed at. Returns the same resolved snapshot as people_getPermissions, so you can confirm the result without a second call. Does NOT create a login — see people_invite. Requires ROLE_ROLES_MANAGER. Write. |
| people_getPermissions | What a person can actually do, resolved: their permission groups (each with the roles it grants), their per-person overrides, and the effectiveRoles the two combine into. THE way to check whether an access change landed — people_list shows a roles field, but this is the one that explains WHY it holds those roles and which lever to pull to change it. Reach for it before every people_setRoleOverrides call, because that tool replaces the override lists wholesale and this is where you read the current ones. staleOverrides are removed-overrides that no longer match any group-granted role, so they currently do nothing. people_list supplies the id. Requires ROLE_ROLES_MANAGER to view anyone but yourself. Read-only. |
Permission Groups
Tools
| permissionGroups_get | Get one permission group by id, including the ROLE_* strings it grants. |
| permissionGroups_list | List the org's permission groups and the roles each one grants — e.g. the "Business Owner" group grants ROLE_ADMIN. Read this before people_setPermissionGroups: the roles in the response are the authority on what a group actually permits, so you never have to guess from its name. |
| permissionGroups_create | Create a permission group (name required; roles = list of ROLE_* strings it grants). Write. |
| permissionGroups_update | Update a permission group's name, description, or granted roles by id. Write. |
Pipelines
Tools
| pipelines_get | Get one sales pipeline by id. |
| pipelines_list | List sales pipelines. A pipeline owns an ordered set of stages — read them with stages_list filtered by pipeline. |
Positions
Tools
| positions_list | List positions — the named roles (e.g. "Backend Engineer") that a project allocation fills. No filters; Position has pagination disabled, so this always returns the org's full role catalog in one call. Each item is {id, name, roles}. Use it to resolve the position name behind an allocations_list row's positionId, and to find the position id a resourcing import must match against. |
Project Members
Tools
| projectMembers_get | Get one project membership by id — its employee, project and position. Ids come from projectMembers_list or the projectMembers array on projects_get. |
| projectMembers_list | List project memberships — WHO CAN SEE WHICH PROJECT. Filter by project (`/projects/{id}`) to read one project's roster, or by employee to read every project one person can reach; each row carries its own id, the employee, the project and the position (employee|tech-lead|account-manager|viewer). Reach for this first when someone reports a project missing from their Projects list or cannot log time against it: an empty roster, or a roster without them in it, IS the explanation — visibility is membership. It is also the id source for projectMembers_update and projectMembers_delete. Note the same person can appear several times on one project, once per position. |
| projectMembers_create | Put a person ON a project (employee + project IRIs required, e.g. "/people/204" and "/projects/243"; optional position = employee|tech-lead|account-manager|viewer, default employee). THIS IS THE ACCESS CONTROL, not a label: a person who is not a member does not see the project at all — it is missing from their Projects list and they cannot log time against it — so this is the tool that restores someone locked out of a project. POSITION IS NOT COSMETIC: a user holding a project-scoped role sees only the projects where their membership position matches it — ROLE_PROJECTS_LEAD matches tech-lead, ROLE_PROJECTS_VIEWER matches viewer — so giving a project lead an `employee` row leaves them just as blind as no row at all. Membership does NOT cascade: putting someone on a parent folder gives them nothing on the projects underneath it, so a folder tree needs one call per project. The unique key is (employee, project, position), which means positions stack rather than replace — a person can hold employee AND tech-lead on the same project as two separate rows, and adding tech-lead to someone who is already an employee there does not remove or upgrade the employee row (use projectMembers_update to change a position in place). Read the current rows with projectMembers_list?project=/projects/{id} first, or projects_get, whose projectMembers array carries each row's id. LOADING A WHOLE ROSTER? A bulk form exists — projectMembers_import reconciles up to 500 memberships in one call, skips the ones already on file so it is safe to re-run, and defaults the notification OFF — but IT IS NOT AVAILABLE ON THIS CONNECTION: it is served only to the internal scope, so you cannot call it here and looking for it will not find it. Loop this tool, or ask your Flowtly operator to run the bulk load. NOT SILENT: adding a person who is not yet on the project dispatches a project-assigned notification to them, so a 17-project backfill sends 17 notifications. Requires ROLE_PROJECTS_MANAGER. Write. |
| projectMembers_update | Change an existing membership's position by id (employee|tech-lead|account-manager|viewer) — get the id from projectMembers_list or the projectMembers array on projects_get. Use this to promote or demote IN PLACE; use projectMembers_create to add a second, additional position alongside the one already held. Changing a position can REVOKE sight of the project for someone whose role is project-scoped (a ROLE_PROJECTS_LEAD demoted from tech-lead to employee stops seeing it). Cannot move a membership to another person or project — delete and recreate for that. Requires ROLE_PROJECTS_MANAGER. Write. |
| projectMembers_delete | Take a person OFF a project by membership id — find it with projectMembers_list or in projects_get's projectMembers array. This REVOKES ACCESS: once the last membership row for that person on that project is gone, the project disappears from their view and they can no longer log time against it, which is exactly how a project silently vanishes for someone. Hours already logged are NOT deleted and stay on the project; the person simply can no longer see or add to them. Deleting one position leaves any other position the same person holds on the same project intact. Requires ROLE_PROJECTS_MANAGER. Irreversible (recreating makes a new row and re-notifies), high-impact. Write. |
Projects
Tools
| projects_costAllocations | How costs were split ONTO this project — which transactions and invoice lines were attributed to it, and in what share. Use it to explain a profitability figure rather than just quote it: this is where an unexpected result is traced back to the document that caused it. Requires ROLE_TRANSACTIONS_MANAGER. Read-only. |
| projects_folderCounts | How many projects sit in each project FOLDER, as folderId + total + active. The folderId is a tagDefinition id — resolve names with tagDefinitions_list, and find which groups are folder groups with tagGroups_list (allowedRelations contains "project"). A null folderId is the uncategorized bucket. Counts root projects only, since folders group roots and phases follow their parent. Read-only. |
| projects_get | Get one project by id — name, type, client, dates, description and price. |
| projects_list | List projects. Filter by type (fixed-price | time-and-material | non-billable | internal), client.name, employee, name, or dateFrom/dateTo ranges. Use it to resolve the project id that tasks, work-time logging, budgets and contracts all take. |
| projects_profitability | THE PER-PROJECT RESULT — what a project earned against what it cost. This is the number a services or development business is usually trying to see, and the one every other project tool feeds. Pass the project id from projects_list. Requires ROLE_ACCOUNT_MANAGER. Read-only. |
| projects_create | Create a project (name + type required; type = fixed-price|time-and-material|non-billable|internal; optional dateFrom/dateTo, client, publicDescription, notes, priceNet). Write. |
| projects_update | Update a project by id (name, type, dates, description, etc.). Write. |
| projects_archive | Archive a project by id — the way to retire a project that cannot be deleted because it has logged time, invoices or budgets hanging off it. Reversible with projects_unarchive. Prefer this over back-dating dateTo, which only makes a project look finished. Write. |
| projects_unarchive | Restore an archived project by id, undoing projects_archive. Write. |
Project Templates
Tools
| projectTemplates_get | Get one project template by id, including its full structure document. projectTemplates_list finds the id. Read this before projectTemplates_update — the structure is written WHOLE, so an update must send the complete document, not a fragment. Read-only. |
| projectTemplates_list | List the org's project templates — reusable blueprints of a project, its phases, its task lists and its tasks. Reach for this BEFORE projects_create when the same shape of project is set up repeatedly (an engagement type, an audit, an onboarding): instantiating a template builds the whole tree in one call, where projects_create makes an empty project you then have to fill by hand. The row flagged isDefault is the org's built-in template, applied to a project created with no template chosen. Read-only. |
| projectTemplates_create | Create a reusable project blueprint from a structure document (version, project, phases, and their lists/tasks). Offsets inside it are RELATIVE — startOffsetDays and durationDays are counted in days from the startDate given at instantiate time, so one template serves every future start. The project.name in the structure is a placeholder; override it per customer when instantiating. The structure is validated server-side against the schema for its declared version, and a violation names the offending JSON pointer. Write. |
| projectTemplates_update | Update a project template by id. The structure column is stored and replaced WHOLE, never merged — send the complete document or the parts you omit are gone. Read the current one with projectTemplates_get first. Changing a template does NOT touch projects already instantiated from it; there is no back-propagation. Write. |
| projectTemplates_delete | Delete a project template by id. Soft delete, and it does NOT touch projects already created from the template — those are ordinary projects and live on. Write. |
| projectTemplates_instantiate | Build a real project from a template — the project, its phases, its task lists and every task, in ONE atomic call. startDate is required and is the anchor every startOffsetDays in the template resolves against. Pass name to override the template's placeholder project name, and client to attach the new project to a customer: instantiating twice against the SAME client is how one customer ends up holding several engagements, each its own project. Returns the created project. Write. |
Resource Request Candidates
Tools
| resourceRequestCandidates_get | One recruitment candidate by id. The id comes from resourceRequestCandidates_list. Needs ROLE_HR_MANAGER. Read-only. |
| resourceRequestCandidates_list | The candidates put forward against hiring requests — people in a recruitment pipeline, not employees available for allocation. Filter by the request id from resourceRequests_list. Needs ROLE_HR_MANAGER. Read-only. |
Resource Requests
Tools
| resourceRequests_get | One hiring request by id, with its position and status. Get the id from resourceRequests_list. HR/recruitment, not resourcing allocation. Needs ROLE_HR_MANAGER. Read-only. |
| resourceRequests_list | Open hiring requests — a request to recruit for a position, in the HR domain. Despite the name this is NOT resourcing allocation demand: it is recruitment. Returns the collection; resourceRequests_get reads one, and resourceRequestCandidates_list gives the people put forward for it. Needs ROLE_HR_MANAGER. Read-only. |
Resourcing Requests
Tools
| resourcingRequests_list | Open resourcing requests — someone asking for a person to be allocated to a project, which is the demand side of resourcing. This is the flow the Resourcing UI's Requests view renders. Do NOT confuse it with resourceRequests_list: that one is HR RECRUITMENT (hiring for a position). Pair it with resourcingRequestsHistory_list for what has already been decided, and resourcingBench_get for who could satisfy a request. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only. |
Resourcing Requests History
Tools
| resourcingRequestsHistory_list | What has already happened to resourcing requests — the decision trail (confirmed, declined, changed) behind the open requests in resourcingRequests_list. Reach for it to answer 'was this already asked for and turned down?' before proposing the same allocation again. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only. |
Responsibilities
Tools
| responsibilities_get | Get one responsibility by id. |
| responsibilities_list | List responsibilities inside a RACI group. Filter by responsibilityGroup. Responsibilities can nest via parent; people are assigned to them through responsibilityEmployees, not directly. |
| responsibilities_create | Create a responsibility inside a group (responsibilityGroup = group id or IRI, + name, required; optional description; optional parent = another responsibility IRI for nesting). Assign people to it via responsibilityEmployees_create. Write. |
| responsibilities_update | Update a responsibility by id (name, description, parent, responsibilityGroup = group id or IRI). Write. |
Responsibility Employees
Tools
| responsibilityEmployees_get | Get one responsibility assignment by id. |
| responsibilityEmployees_list | List who is assigned to which responsibility, and at what percentage. Filter by employee to read one person's entire RACI load across every group. |
| responsibilityEmployees_create | Assign an employee to a responsibility (responsibility = responsibility id or IRI, employee = employee id or IRI, percentage 0-100, all required; optional targets and description). Write. |
| responsibilityEmployees_update | Update a responsibility assignment by id (percentage, targets, description). Write. |
| responsibilityEmployees_delete | Remove an employee's assignment from a responsibility by id. Write. |
Responsibility Groups
Tools
| responsibilityGroups_get | Get one responsibility group by id. |
| responsibilityGroups_list | List responsibility groups / RACI areas — the top-level "Odpowiedzialności" items, each with an accountable person. Individual responsibilities hang underneath them. |
| responsibilityGroups_create | Create a responsibility group / RACI area (name required; optional description and responsibleEmployee = the accountable person, given as a plain employee id like 6 (from people_list) or the /people/6 IRI). This is the top-level 'Odpowiedzialności' item. Add individual responsibilities under it via responsibilities_create. Write. |
| responsibilityGroups_update | Update a responsibility group by id (name, description, responsibleEmployee = employee id or IRI). Write. |
Schedule Employees
Tools
| scheduleEmployees_get | One schedule-to-employee assignment by id. The id comes from scheduleEmployees_list. Needs ROLE_SCHEDULES_MANAGER. Read-only. |
| scheduleEmployees_list | Which employees are assigned to which working-time schedules. Use it to go from a schedule (schedules_list) to its people, or to find the schedule a given employee follows. Needs ROLE_SCHEDULES_MANAGER. Read-only. |
Schedule Plan
Tools
| schedulePlan_list | The schedules in force on ONE given date — pass the date in the path. Reach for it to answer 'who is working today / on this date' without reading every schedule and resolving its ranges yourself. Unlike the other schedule reads this only needs ROLE_USER, so it is the one available to an ordinary employee. Read-only. |
Schedule Ranges
Tools
| scheduleRanges_get | One schedule time range by id. The id comes from scheduleRanges_list. Needs ROLE_SCHEDULES_MANAGER. Read-only. |
| scheduleRanges_list | The time ranges that make up working-time schedules — the actual hours a schedule covers. Read the parent with schedules_get first; this expands its ranges. Needs ROLE_SCHEDULES_MANAGER. Read-only. |
Schedules
Tools
| schedules_get | One working-time schedule by id, with its ranges and assigned employees. The id comes from schedules_list; scheduleRanges_list and scheduleEmployees_list read its parts. Needs ROLE_SCHEDULES_MANAGER. Read-only. |
| schedules_list | Working-time schedules — the shift/working patterns an org defines, NOT project allocation. Use resourcingSchedule_get for who is booked on what; use this for the working patterns themselves. schedules_get reads one by id. Needs ROLE_SCHEDULES_MANAGER. Read-only. |
Stages
Tools
| stages_get | Get one deal stage by id. |
| stages_list | List deal stages, in order. Filter by pipeline. deals_create requires a stage id from here, and moving a deal between stages is what dealStageHistories records. |
Suppliers
Tools
| suppliers_list | List suppliers/contractors — served from /contractors, so "supplier" and "contractor" are the same record. Filter by cyclic for recurring suppliers. Use it to resolve the supplier that a cost, a contract or an incoming invoice is filed against. |
| suppliers_create | Create a new supplier/contractor record (name, tinType, costGroup required). Write. |
| suppliers_update | Update a supplier/contractor's details (name, tax id, payment terms, etc.) by id. Write. |
Tag Definitions
Tools
| tagDefinitions_list | List tag definitions — the tags that can be attached to records, each inside a tag group. tags_create takes a tagDefinition id from here plus the record to attach it to. |
| tagDefinitions_create | Create a tag definition (name, level, tagGroup required) within a tag group. When the group's allowedRelations contains "project", each definition here IS a project folder — this is the tool that creates one. Write. |
Tag Groups
Tools
| tagGroups_list | List tag groups — the containers that organize tag definitions. |
| tagGroups_create | Create a tag group (name required) to organize related tag definitions. Also how you create a PROJECT FOLDER container: pass allowedRelations: ["project"] and the group's definitions become folders in the projects list. A group with empty allowedRelations is universal and is NOT treated as a folder. Write. |
Task Comments
Tools
| taskComments_list | List comments on project tasks, oldest first. Filter by task to read one task's discussion. |
| taskComments_create | Add a comment to a project task (task id + content). Write. |
Task Lists
Tools
| taskLists_list | List task lists — the board columns/sections tasks are filed into. Filter by project. tasks_create takes a list id from here. |
Tasks
Tools
| tasks_get | Get one project task by id — title, project, status, list, assignees, dates and recurrence. |
| tasks_list | List project tasks. Filter by project, list, status, assignees, isTemplate, or startAt/dueAt ranges. Recurring tasks expose recurrenceParent and recurrenceRule, so a generated occurrence can be traced back to the rule that produced it. To decide whether a task is DONE, compare its status against taskStatuses_list (isClosed) rather than matching on the status name. |
| tasks_create | Create a project task (title + project required; optional status, list, assignees, dueAt, priority). Write. |
| tasks_update | Update a project task by id — change status (incl. mark done), assignees, dueAt, title, etc., or MOVE the task to another project by passing `project` (re-parenting; the task list is cleared unless you also name a `list` in the target project, because a list belongs to one project). Write. |
Task Statuses
Tools
| taskStatuses_list | List the project task statuses, in board order. isClosed marks the done states and isDefault the status a new task gets. Read this before interpreting a task's status — the names are org-configurable, so "Done" is not a reliable string to match on. |
Tax Groups
Tools
| taxGroups_list | List tax groups. Use it to resolve the taxGroup id that taxRules_list filters on and that invoice rows carry. |
| taxGroups_create | Create a tax group (name + type required). Write. |
| taxGroups_update | Update a tax group's name or type by id. Write. |
Tax Rules
Tools
| taxRules_list | List tax rules — the rates and the periods they apply to. Filter by taxGroup. |
| taxRules_create | Create a tax rule. Write. |
| taxRules_update | Update a tax rule by id. Write. |
Transactions
Tools
| transactions_list | List bank transactions — the bank feed that incoming invoices are matched against. Filter by bankAccount, counterpartyRole, cost, ignored, hasDetectedProblems, an orderDate/execDate range, or amount.between. Note orderDate and execDate are different: a payment can be ordered in one month and execute in the next. |
| transactions_suggestions | Read Flowtly's proposals for one bank transaction — which counterparty, cost group or document it should be filed against. The mirror image of incomingInvoices_suggestions, from the money side. |
| transactions_importStatement | Import a bank statement file (e.g. an MT940 .sta file) — pass each file's raw text content verbatim (NOT base64) with a filename. THERE IS NO bankAccount PARAMETER: the backend routes a file by stripping all non-digit characters from your bank accounts' numbers and from the file's bytes, and importing into every account whose digits appear anywhere in the file — so one file can land in several accounts, and a statement for an account that is not set up in Flowtly (or whose number is recorded differently than the bank writes it) imports into none of them, failing with an error that explains exactly why — read that message, it is the only diagnostic this endpoint gives you. On success the response is `{ imported, matching }`: `matching: "in_progress"` means contractor/attachment matching for the new rows is still running after this call returns, so an immediate transactions_list may show rows not yet matched — re-read a little later for the final state. Re-importing the same statement does not create duplicate rows; the importer recognizes transactions it has already seen. Once a statement is in, point an existing no-bank-line payment at one of its rows with invoiceTransactions_update. Write. |
| transactions_delete | Delete a bank transaction by id — find it with transactions_list. Reach for this ONLY to undo a bookkeeping mistake that cannot be corrected any other way: a statement imported against the wrong bank account, or rows entered by hand before the real statement arrived and now duplicated by it. A transaction is a record of what the bank did, so deleting one on an imported account makes the ledger disagree with the bank; the backend allows it for ROLE_ADMIN only (a transactions manager can delete solely on cash/manual accounts). BEFORE deleting a suspected duplicate, prove the pair: match the imported row on amount AND invoice number AND counterparty, not amount alone — a payment that arrived after the statement's end date has no counterpart and deleting it destroys the only record of that income. The backend DETACHES rather than deletes what hangs off it: invoice payments survive with their bank line cleared (re-point them with invoiceTransactions_update), attachments and properties are unlinked, while project and employee transaction rows are removed with it. Irreversible, high-impact. Write. |
Work Times
Tools
| workTimes_get | Get a single work-time entry by id — date, minutes, project, notes and the employee it belongs to. |
| workTimes_list | List work-time (logged hours) entries. Filter by date range (date.after / date.before, YYYY-MM-DD) and optionally by employee or project; page with cursor. Each row carries employeeId/employeeName and projectId/projectName, so this is how you export all logged hours for a period. IMPORTANT: org-wide results require ROLE_WORKING_HOURS_VIEWER. Without it the backend does NOT error — it silently returns only the connected user's own entries, so an "everyone's hours" export can come back containing one person and look perfectly fine. If every row belongs to one employee and you did not filter by employee, the response carries a scopeWarning saying so — surface it to the user rather than presenting the result as org-wide. |
| workTimes_log | Log a work-time entry for the connected Flowtly user (date, durationMinutes, project, notes). THE NOTE MUST CLEAR THE SERVER’S THIN-DESCRIPTION CHECK, which a batch backfill hits repeatedly: it needs EITHER about 32 characters (the exact floor is a per-organisation setting, and an org may set it to 0 to switch the check off) OR a “#” ticket reference OR an http(s) link — any one suffices. “Flowtly – Scallier” is refused; “Flowtly – Scallier #FLOW-123” is not. The 422 names propertyPath `description`, which is the server’s name for the field this tool calls `notes`. Write. |
| workTimes_update | Correct one logged work-time entry by id — its date, minutes, project or description. This is how a mis-filed entry is MOVED between projects: workTimes_log only ever creates, so without this a wrong project or a typo in the description is permanent. Read the entry first with workTimes_get. The same thin-description check applies as on workTimes_log: roughly 32 characters — the floor is a per-organisation setting and may be 0, which disables it — OR a “#” ticket reference OR an http(s) link, any one of the three. Write. |
| workTimes_delete | Delete one logged work-time entry by id. For a duplicate or an entry logged against work that never happened — prefer workTimes_update when the entry is real but wrong, so the hours stay in the record rather than vanishing from it. Logged hours feed project financials and utilisation, so a delete quietly changes reported numbers for a past period. Write. |
Client Contacts
Tools
| clientContacts_create | Create a contact person for a client (client, type, name, email required). Write. |
Counterparty Bank Accounts
Tools
| counterpartyBankAccounts_create | Attach a bank account to a counterparty (counterparty + accountNumber). Write. |
Payment Schedule Lines
Tools
| paymentScheduleLines_import | Load a contract's whole instalment plan in one call, instead of one round trip per line. Built for developer contracts, which are paid in construction tranches — a single sale is six to twelve instalments, and a register of them is hundreds. Each row names its contract BY NAME (for an imported developer contract, its agreement number), a due date, and an amount in MINOR UNITS — grosze, so 5 300,00 is "530000" and "5300" silently books 53,00. Rows reconcile against the lines already there on contract+date+amount+note, so an unknown line is created, an identical one is skipped, and re-running the same batch changes nothing; PaymentScheduleLine has no external-reference column, so that natural key is the reconcile key. A row whose contract name matches nothing, or matches MORE than one contract, is reported failed rather than attached to a guess — putting an instalment on the wrong contract misstates two cashflows at once. Pass dryRun:true first on a real load. Max 1000 rows. Write. |
| paymentScheduleLines_create | Add one instalment to a contract's payment schedule — the plan of what is expected to be invoiced or paid, and when. Pass the contract IRI, a date and an amount. This is what clears the missing-payment-schedule problem contracts_get reports on a non-cyclic contract: a one-shot fee still has a schedule, it is simply a single line for the whole amount on the day it falls due. Cyclic contracts are not checked for one, because the system does not auto-generate lines from a cadence. AMOUNT IS IN MINOR UNITS — grosze, not złote: 5 300,00 is "530000", and "5300" silently books a 53,00 line. The API returns them the same way, so read one back with contracts_paymentScheduleLines if you are unsure of the scale. Read the result back with contracts_paymentScheduleLines. Write. |
| paymentScheduleLines_update | Change one payment schedule line by id — its date, amount or note. Use it when an instalment slips or is renegotiated, rather than deleting and recreating, so the line keeps any invoice already matched to it. AMOUNT IS IN MINOR UNITS — grosze, not złote: 5 300,00 is "530000", and "5300" silently books a 53,00 line. The API returns them the same way, so read one back with contracts_paymentScheduleLines if you are unsure of the scale. Write. |
| paymentScheduleLines_delete | Remove one payment schedule line by id. Deletes the PLAN, not the money: an invoice or transaction already matched to the line is not affected, but it stops being reconciled against anything. Prefer paymentScheduleLines_update for an instalment that moved. Write. |
Organization Logo
Tools
| organizationLogo_upload | Upload/replace the organization's logo (base64 image + contentType + filename). Read the current one via configs_get organization-logo-url. Write. |
Organization Icon
Tools
| organizationIcon_upload | Upload/replace the organization's icon/favicon (base64 image + contentType + filename). Read the current one via configs_get organization-icon-url. Write. |
Storage
Tools
| storage_upload | Attach a file to any record Flowtly's generic storage accepts — an ASSET (relationName "property"), a project, a task, a client, a location, a contractor, an invoice. This is the only route to an asset PICTURE: uploading with relationName "property" sets the image the app shows for that asset (served as `file` on the asset payload). Property has no image column -- the picture is derived from this table at read time, which is why nothing on the entity hints it exists. It is ONE slot and the newest upload wins, so a second image replaces the first rather than adding to a gallery. The same holds for location, invoices and transaction-attachments; clients, agreements and candidates instead accumulate every upload under `files`. EVERY OTHER RELATION LINKS THE UPLOAD TO NOTHING VISIBLE, and relationName "employees" is the one to be careful with: it stores the bytes and creates NO Document, so People > Documents stays empty and the employee payload carries no file. The /documents route on any record returns Document entities, and an upload creates none — which is how signed NDA and ESOP PDFs came to be reported as filed while the Documents tab showed nothing (#255). A real employee document needs POST /documents carrying a DocumentType whose relationName is `employee`, the employee id, and the IRI of the Storage row this call returns. DO NOT ASSEMBLE THAT BY HAND: use employeeDocuments_createUploadTicket, which does all three steps — stores the bytes, creates the Document, and reads it back through /people/{id}/documents — and reports stored / linked / verified separately. This tool stops at the bytes. Do not reach for agreementTypes.* either: an AgreementType is the type of an employment CONTRACT (Umowa o pracę, Umowa zlecenie) under Ludzie > Umowy, and is not a DocumentType. Report bytes stored, business record linked and visibility verified as three separate claims, and assert only the ones you actually did. `file` is omitted from LIST responses unless the request passes ?include=file, so read one record back to confirm the picture landed. Pass relationName + relationId (the id from that record's list tool; a /assets/7 IRI is accepted and reduced) plus the bytes as base64 with a contentType and filename. SIZE LIMIT: the bytes travel as base64 inside this call, so keep it under roughly 150 KB — photographs are almost always past that, and for those use storage_createUploadTicket, which has no ceiling. Permissions are whatever editing the OWNING record needs: the backend resolves relationName to that entity and asks its own voter, so filing against an asset needs the assets permission, against a client the clients one. For a contract, prefer contractAttachments_create instead — it also clears contracts.problem_missing_document, which this does not. Write. |
| storage_createUploadTicket | Mint a short-lived, single-use ticket for attaching a LARGE file to any record — the way asset PICTURES actually get in, since an image is always past the base64 ceiling. On property/location/invoices/transaction-attachments the newest upload becomes the record's visible image, replacing the previous one; on clients/agreements/candidates uploads accumulate. Use this instead of storage_upload whenever the file is more than a few tens of KB: that tool carries the bytes as base64, which a caller has to emit as text, and a 400 KB JPEG becomes ~533 K base64 characters, far beyond what fits in one response. Pass relationName + relationId plus a filename; you get back an uploadUrl and a ready-to-run curl. Then send the file's RAW BYTES to that URL (curl --data-binary @photo.jpg) — not base64, not multipart — and the response carries the created Storage record. The ticket expires in 15 minutes, works once, and can only file against the one record it names. Write. |
Contract Attachments
Tools
| contractAttachments_create | Attach a document to a contract — normally the executed PDF, or an annex (DPA, SLA, price annex) filed alongside it. Pass the bytes as base64 with a fileName and the contract id from contracts_list; `contractId` here is a BARE id, unlike the IRIs contracts_update takes for counterparty and project, though a full /contracts/<id> IRI is accepted and stripped. SIZE LIMIT: the bytes travel as base64 inside this call, so the whole document has to fit in one model response — keep it under roughly 150 KB, and for anything larger use contractAttachments_createUploadTicket instead, which is built for exactly this and has no such ceiling. An executed contract with a signature card is usually well past that (673,617 bytes becomes 898,156 base64 characters, several times what one response can carry), and no error comes back when it does not fit, because the call cannot be emitted at all — the request never reaches the server, so check the file size BEFORE starting rather than discovering it by failing. This is what clears the missing-document problem contracts_get reports, so a contract maintained over the API stops sitting in the app's needs-tidying queue. One signed document can back several contract rows (a deal with both a recurring and a one-shot part is two rows, because `cyclic` is per-record) — call this once per contract id with the same bytes. What happens next depends on kind. kind "contract": `status` comes back "analyzing" and the backend reads the document asynchronously, normally within a few minutes; poll contracts_get until the attachment is "analyzed" or "failed" (a failure carries failureReason and failureRetryable). The reading only FILLS EMPTY fields of the contract and never overwrites a name, direction, amount, dates, currency, payment terms, schedule lines or prices already on it; every extracted value stays in the attachment's analysisSummary, and analysisSummary.notApplied lists what it left as a suggestion. kind "annex": stored and NOT analysed; `status` is "stored", which is final, and the contract does not change. Treat analysisSummary as a SUGGESTION to check rather than a fact to trust. Write. |
| contractAttachments_createUploadTicket | Mint a short-lived, single-use ticket for attaching a LARGE document to a contract — the executed PDF, or an annex. Use this instead of contractAttachments_create whenever the file is more than a few tens of KB: that tool carries the bytes as base64, which a caller has to emit as text, and a real signed contract (~700 KB, ~900 K base64 characters) is far beyond what fits in one response. Pass the contract id from contracts_list plus a fileName; you get back an uploadUrl and a ready-to-run curl. Then send the file's RAW BYTES to that URL (curl --data-binary @file.pdf) — not base64, not multipart — and the response is the created attachment. The ticket expires in 15 minutes, works once, and can only attach to the one contract it names. This is what clears the missing-document problem contracts_get reports. Write. |
Invoice Transactions
Tools
| invoiceTransactions_create | Record a payment against an outgoing (sales) invoice. `invoice` is an invoice IRI from invoices_list; `date` is when the payment is treated as made. `transaction` is optional — omit it to record settlement with no bank line, which is what you want for historical invoices whose bank statement was never imported. `amount` is optional and defaults to the invoice's outstanding amount. Recording a payment is what stops an issued, past-due invoice being treated as unpaid, so it is also what stops payment reminders being queued for it. Nothing prevents recording two payments against one invoice, so read invoices_get first if you are unsure whether one is already settled. Write. |
| invoiceTransactions_update | Update an existing invoice-payment record by id (from invoices_get's invoiceTransactions, or by paging invoiceTransactions). The most common use: point a payment recorded with no bank line at a transaction you just imported via transactions_importStatement, by setting `transaction` to a transaction IRI/id from transactions_list. THE FOOTGUN: this is a PATCH, but the backend still requires `invoice` and `date` on every call — it does NOT merge in the existing values for you. Read the record first (or already have it from the create call) and resend its `invoice` and `date` unchanged alongside whatever you actually mean to change, or the update is rejected. `transaction` accepts null to unlink a payment from a bank line. `amount` is optional. Write. |
| invoiceTransactions_delete | Delete a payment record from an invoice by id — read the ids from invoices_get's invoiceTransactions. This removes the RECORD THAT AN INVOICE WAS PAID, not a bank transaction: use it when an invoice carries a payment that should never have existed, the usual case being the same payment booked twice — once by hand and once by the statement import that later matched it. Check invoices_get first and delete the record whose `transaction` is the wrong one (keep the one pointing at the real imported bank line); deleting the last remaining payment makes the invoice unpaid again, which re-arms payment reminders for it. Requires ROLE_INVOICES_MANAGER. Irreversible, high-impact. Write. |
Resourcing
Tools
| resourcing_importTimeline | Import a resourcing allocation timeline sheet (fetch it via the Drive MCP, pass its CSV verbatim). This is a FULL-REPLACE mirror of the org's Allocation rows for `year`: rows in the sheet are created/updated, and any existing row for that year absent from the sheet is DELETED — not a merge. DRY-RUN BY DEFAULT: an omitted dryRun previews and writes nothing; pass dryRun:false to apply. The report gives `created` / `replaced` plus `unmatchedPeople` / `unmatchedProjects`. TWO THINGS ARE EASY TO MISS: a sheet row whose project does not resolve is SKIPPED while the call still reports success, so a green result can hide a partial import; and a role code the position catalogue does not already hold is CREATED as a new position rather than rejected — see `createdPositions`. Both are called out in `warnings` when they happen; surface that to the user rather than reporting only `created`. A sheet that parses to zero rows is refused (it looks exactly like a bad read about to wipe the whole timeline) unless you pass force:true. Read allocations_list afterwards to see what landed. High-impact. Write. |
Organization
Tools
| organization_whoami | Return the organization this MCP connection is bound to — { orgId, name, slug, userId }. Call it to confirm WHICH tenant you are about to write into before any create/update: the connection is pinned to exactly one org by the token, and writing prospects/records into the wrong org is a real incident. Read-only. |
Resourcing Actuals
Tools
| resourcingActuals_get | Reported hours vs the plan, per person per week, over a from/to window — the 'is the team actually on plan?' question, which NO other resourcing tool answers: allocations tell you what was PLANNED, this tells you what was DELIVERED. Returns week columns plus one row per person (planned %, reported %, variance, totals, and a per-project breakdown). reportedPercent null means 'no contract that week' and 0 means 'a contract existed and nothing was reported' — do NOT collapse the two. Pass financials for revenue/cost/margin, which are omitted otherwise. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only. |
Resourcing Bench
Tools
| resourcingBench_get | Who is NOT staffed over a from/to window — the bench. Reach for it when asked who to put on a new project or where capacity is going unused; resourcingActuals_get tells you how loaded people are, this tells you who has no load at all. IT DOES NOT KNOW ABOUT LEAVE: freePercent is 100 minus confirmed allocations, nothing else, so someone on three weeks' approved holiday reads 100% free and no field on the response says otherwise. Answering 'who is available' from this alone will put people on projects while they are away — cross-check holidays_active or holidays_list. Needs the resourcing module. Read-only. |
Resourcing Schedule
Tools
| resourcingSchedule_get | The planned resourcing schedule over a from/to window — the allocation timeline as the planner shows it. Use it for what is BOOKED going forward; use resourcingActuals_get for what was actually reported against it. Needs the resourcing module and ROLE_RESOURCING_MANAGER. Read-only. |