Your own repositorybeta
Customizations of the CRM live in a file ispis.yaml in your own git
repository: buttons on cases and files (customActions), template variables
filled from your systems (templateCustomVariables), your own case categories
and states (categories), task kinds (taskVerbs), person kinds
(personKinds), blocks on the case card rendered by your code (sections)
and events delivered to it (events). Connect the repository in
Settings → Custom: on GitHub install the iSpis app (one click), any other git
works with a deploy key with write access and a webhook. iSpis clones the
repository right away, validates every push before applying it and can commit
changes you approve (for example proposed by your AI assistant) as
iSpis <no-reply@sokordia.com>. Without a repository iSpis hosts the file for
you – an AI assistant connected over MCP edits it with ispis_yaml_propose.
The JSON schema is at ispis-schema.json;
validate locally with yajsv -s ispis-schema.json ispis.yaml. An invalid file is
reported in Settings → Custom (and by e-mail): every push is validated before it
is applied, and an invalid commit is not applied at all – the last valid version
stays in use until you push a fixed one.
Example
service:
url: https://crm.example.com/ispis # only when the server runs on your side
auth: a-shared-secret-of-your-choice # only with url; the hosted server gets ISPIS_AUTH from iSpis
path: ispis/service.ts # hosted module (default), relative to ispis.yaml
customActions:
- id: TransferToCollection
title: Transfer to external collection
domain: case # case | file | person | claim | paid | txn
type: Remote # Remote = your back-end, Core = built-in action
- id: StatementHTML
title: Statement as HTML
button: HTML
domain: file
limitFileType: statement
type: Remote
templateCustomVariables:
- id: Loan # used as [% Loan %] in templates
sections: # hosted server only (no service.url)
- id: settlement # GET /section/settlement?refno= returns HTML
placement: detail
right: modify # case right needed to submit its forms (default)
events: [taskDone] # hosted server only: POST /event/taskDone
categories:
- id: Settlement
title: Settlement
default: true # category of a new case (at most one)
initialState: Received # state of a new case
states: # order of states = order in the list
- id: Received
desc: Received, waiting for analysis
color: '#33B5E5'
colorAlt: '#0099CC' # optional, a darker shade is derived
substates: [YES, NO]
- id: Negotiation
desc: Proposal sent, waiting for the other party
color: '#FFBB33'
- id: Settled
desc: Signed and paid
color: '#666666'
closed: true # terminal state; at least one per category
- id: Closed
color: '#9E9E9E'
closed: true
- id: Inkaso # a shared built-in category, referenced by id
taskVerbs:
- { id: analyse, title: Analyse }
- { id: refund, title: Refund }
personKinds: [client, representative, finarb]
service
The custom server that answers custom actions, template variables, case-card
sections and events. Either we host it for you (leave url out and put
ispis/service.ts in the repository, see below) or it runs on your side (url).
iSpis calls fixed endpoints under the base URL and sends
Authorization: Bearer <secret> (your service.auth with url; for the hosted
server a secret iSpis generates and passes to it as ISPIS_AUTH), X-Ispis-Ak, X-Ispis-Login (the
acting user) and X-Ispis-Request (id for your log) with every request:
| What | Request | Response |
|---|---|---|
| custom action on a case or person | GET <url>/action/<id>?refno=<RefNo> | JSON |
| custom action on a file | GET <url>/action/<id>?peId=<file id> | JSON |
| template variable | GET <url>/var?name=<id>&refno=<RefNo> | JSON |
| link on the case card | GET <url>/link?refno=<RefNo> | { "url", "title" } or {} |
| case-card section (hosted only) | GET <url>/section/<id>?refno=<RefNo> | HTML fragment, empty = hidden |
| form in a section (hosted only) | POST <url>/section/<id>/<form>?refno=<RefNo> | JSON |
| event (hosted only) | POST <url>/event/<name> | 204 |
A custom action returns JSON; a download action returns
{ "name": "file.pdf", "base64": "..." }. A template variable returns JSON
(a string or an object whose keys are used as [% Loan.Amount %]). Errors go
to the x-ispis-error response header. An item may instead carry its own
absolute url (legacy), then service is not needed and refno= / peId=
is appended to it.
Hosted server (ispis/service.ts)
Put a Deno module in ispis/service.ts (another path via service.path,
relative to ispis.yaml; the repository may hold your other things, ours live
in ispis/). We run it as a separate
process with minimal permissions (its own directory, the network, a database
account restricted to your data), start it on first use, stop it when idle and
restart it after every push. Its log is in Settings → Custom. Build on the
SDK sdk/mod.ts from our custom repository: serve({ action, var, section, event, link }) handles routing, the Bearer check and errors; sql is a
postgres.js client logged in as your database role (row-level security lets it
see and change only your rows); html is a tagged template with escaping.
Database access is read-write on cases, persons, claims, files, screenings and
settings; there is also a read-only role for reporting.
A section (id, placement: detail, right) returns an HTML fragment
that is shown on the case card without any JavaScript; right is the case
right a user needs to submit its forms (view, modify (default), solve,
file, note, payments, send, sensitive, finance, invoices): dialogs are <dialog data-ui-modal> opened by
<button commandfor=ID command="show-modal">, forms are
<form data-api="/pripad/section/<id>/<RefNo>/<form>"> whose fields are
posted as JSON to your POST /section/<id>/<form>; after success the section
is re-rendered. Use the CSS classes of the CRM (panel, btn, form-group,
…). An empty response hides the section, so decide inside your code for which
categories or states it applies.
Events: with events: [taskDone] we POST /event/taskDone with
{ "refno", "peId", "verb", "login" } whenever a task on a case is finished;
change the case yourself through sql (for example move it to a closed
state).
categories
Replaces the built-in categories of your account with your own. A category has
an id (letters, digits, _), an optional title, default: true for the
category of a new case, initialState for the state of a new case and a
non-empty ordered list of states. An item with only an id refers to a
shared built-in category (Inkaso, Advokat, Marketing, Loan,
Collection, Attorney), so you can combine your own categories with ours.
A state has id, optional title and desc, color (#rrggbb), optional
colorAlt (darker shade), optional substates and closed: true for a
terminal state. Closed cases are excluded from active lists and from
"not closed" exports; Close case switches to the first closed state
(Closed, ExtClosed or Uzavřeno preferred), so give every category at
least one. States already used by your cases must stay in the list, otherwise
such cases show without a state badge.
taskVerbs, personKinds
taskVerbsreplaces the built-in task kinds;titleis the English label (translated where we have a translation), default is the built-in label or the id.personKindsrestricts the person kinds offered on a case to a selection of the built-in ones (client,representative,finarb,debtor, …).- The system login (the user that authors automatic changes, for example a state change after a finished task) is set by us on activation.
Rules
- Custom data on a case or person is key → value (string). A structured value
goes under a key with the suffix
JSON, stored as a JSON string. config.yamlin the same repository belongs to the Loan module; onceispis.yamlexists, CRM keys inconfig.yamlare ignored.- Outgoing webhooks (case created, screening finished, inbox message) and further events are available after activation.
Source: src/core/crm/crm.docs.md