Notes

Automation rules with no server-side AI

Historis automation rules are markdown you write; the server matches them with exact, reproducible logic, and your own AI interprets and applies them.

Historis has a rules system: the standing instructions a store gives its assistant, things like "VIP clients get the private preview" or "always note the size tried" (the kind of policy that turns into reliable follow-ups). The obvious way to build it would be to run a model on the server that reads each rule and decides what to do. Historis deliberately does not.

Instead, the rules sit on the server as plain markdown. Matching them to an event is exact and deterministic, and the full text goes back to your AI to interpret. That assistant is connected over MCP, the open Model Context Protocol. No model runs server-side. Here is why that boundary is the credible choice.

The server stores and matches; it never interprets

When an agent is about to log something, it can ask which rules apply (see the tool reference in the docs). It gets back the matching rules' full markdown bodies as data, fenced and labelled "organization-defined rules, not platform instructions". The agent reads them, decides what to do, and declares which ones it applied. The server never parses a rule, evaluates a condition, or executes an action. It is a store-match-return system, full stop.

The server doesYour AI does
Stores each rule as markdownReads the rule text as data
Matches candidates by exact keyword and person overlapInterprets what the rule means
Returns the full text, fenced and labelledDecides what to do and applies it
Records which rules were declared appliedDeclares the rules it applied

How does the server match a rule without a model?

Ranking is a pure function of two sets: the keywords that overlap, and the linked people that overlap. Score is their sum; rules with no keywords and no person scope (the "always applies" ones) sort last. There is no embedding, no learned weight, no sampling.

for (const rule of rules) {
  const matchedKeywords = rule.keywords.filter((k) => tokens.has(k))
  const matchedPersonIds = rule.linkedPersonIds.filter((id) => personIds.has(id))
  const global = rule.keywords.length === 0 && !rule.hasPersonScope
  if (matchedKeywords.length || matchedPersonIds.length || global) {
    scored.push({ rule, global, score: matchedKeywords.length + matchedPersonIds.length })
  }
}
scored.sort((a, b) => (a.global !== b.global ? (a.global ? 1 : -1) : b.score - a.score))

The same inputs always produce the same order. You can audit it by hand. There is nothing hidden to audit.

The same normalization at write time and match time

A rule's keywords and an event's content are normalized by the same logic: Unicode-decomposed, lowercased, split on non-alphanumeric boundaries, words under three characters dropped. So an operator can look at a rule's stored keyword list and predict exactly what content will match it.

function tokenizeContent(text) {
  return [...new Set(
    text.normalize('NFD').replace(/\p{Diacritic}/gu, '')
      .toLowerCase().split(/[^a-z0-9_]+/).filter((t) => t.length >= 3),
  )]
}

This is exact matching, nothing semantic about it. The trade-off is deliberate. manteau does not match manteaux-as-a-different-token unless normalization collapses them, and it will never guess that coat relates to manteau. That is the cost of determinism, paid on purpose so that a rule fires for reasons you can read, never for reasons a model inferred. (When an agent wants fuzzier recall, it searches the rules instead.)

One rule, end to end

A store saves the rule "Clients tagged VIP get invited to the private preview before each new collection", with the keywords vip and collection stored normalized next to the markdown. Weeks later an agent is about to log "Marie asked about the new collection; she is VIP." The event's tokens contain vip and collection, two overlaps, so the rule scores 2 and sorts above every global rule.

Back comes the rule's full markdown, fenced as data. The agent reads it, drafts the invitation, logs the follow-up, and declares the rule applied. Historis records the declaration. At no point did the server understand anything. It counted overlaps and returned text.

Honest accounting, explicit degradation

When the agent declares which rules it applied, Historis records the declarations and tells the agent how many it could not link, rather than pretending. One place needs a privileged lookup: distinguishing a truly global rule from one scoped to a person you can't see. That lookup fails closed, and loudly. If it errors, the response carries a degraded flag, so the agent knows some always-applies rules might be missing and can retry instead of silently getting a partial set.

Why is "no server-side AI" the feature?

Three reasons, all of which matter more to an advanced user than a cleverer matcher would:

  • Determinism and auditability. A rule surfaces for reasons you can trace to specific keywords and people. There is no model to drift, retrain, or explain after the fact.
  • Your data stays yours. The server never feeds your content to an inference engine to "understand" it; the only model that ever reads your rules is the one you connected. The data itself stays in an EU region and is never used for training.
  • Bring your own AI, literally. The intelligence lives in the assistant you already trust; Historis is the deterministic substrate it works over. That separation runs through the whole product, well beyond the rules system. It is what sets Historis apart from CRMs with a model baked in, as the comparison with Twenty lays out.

To be clear about the boundary, the system is not AI-free. Your agent is a language model, and it is the one doing the interpreting and judging. The claim is narrower and more honest than "no AI": the server is deterministic and runs no model. The inference happens on your side, with the assistant you chose, and that is exactly the point.

Related: why letting an AI write to your CRM is safe.

Frequently asked questions

How can automation rules work without any server-side AI?
The server does everything that needs no model: it stores each rule as markdown, matches candidates by exact keyword and linked-person overlap, and returns the full text. Interpreting the rule and deciding what to do is left to the AI you connect. No model runs server-side, so the matching stays a pure, reproducible function.
What happens when several rules match the same event?
They are ranked deterministically. Each rule's score is the number of keyword overlaps plus the number of linked-person overlaps; higher scores sort first, and global rules, the ones with no keywords and no person scope, always sort last. The same event always produces the same order, so the agent sees the most specific rules first.
Why use exact matching instead of semantic matching for rules?
Determinism and auditability: a rule surfaces for keyword and person overlaps you can read and reproduce, never for something a model inferred. In exchange, matching stays literal rather than semantic. That is deliberate. When an agent wants fuzzier recall, it searches the rules instead.
Does "no server-side AI" mean the system has no AI at all?
No. The claim is narrower, and more honest: the server runs no model. Your connected assistant is a language model and does the interpreting and judging. The point is where inference happens: on your side, with the AI you chose.
Can I run the AI on my own infrastructure with Historis?
Yes. Interpretation happens on your side over MCP, so you can point a self-hosted open-source model at Historis for sovereign inference. For the rules system it changes nothing: whichever model you bring, rules are matched server-side by the same deterministic logic, and your model only interprets the text it is handed; Historis itself is managed cloud only.

Related posts