Download all docs
modifiers

ICP

A saved description of the company or contact worth pursuing — a declarative ruleset that consumer ops like sales-board's ingest-icp-matches read by reference to filter raw research leads down to the ones that become Pursuits.

Working with it

Selecting a ICP reveals its settings in the properties panel; it has no dedicated full-screen workbench.

How it appears

The same element type rendered as a definition, a circle instance, and a live workspace card.

Ic
type

ICP

Ideal Customer Profile — declarative match rules that filter leads into pursuits

modifiersmodifierdefinition

When to use / not

When to use

  • Encoding who a sales-board should chase — industries, regions, size/revenue bands, buying signals, target roles — so lead ingestion promotes only the matches into Pursuits.
  • Reusing a saved LinkedIn search URL as the filter itself (URL mode), routed through the circle's linkedin element at ingest time.
  • Keeping several audiences side by side in one circle — an enterprise ICP, an SMB ICP, a campaign ICP — and selecting which one a consumer op uses by reference.

When not to use

  • Filtering or validating arbitrary request/response payloads — ICP never sits in a middleware path; use validation or filter-words for that.
  • Storing a reusable scalar or config value with no match semantics — that is what variable is for.
  • Actually sourcing and opening pursuits — the matching logic lives on the consumer (sales-board.ingest-icp-matches); ICP only declares the rules.

Topology

Attaches to another element as a modifier, shaping that element's behaviour rather than running on its own.

Properties

match_modestring
How populated fields combine. 'all' = AND (every rule must match), 'any' = OR (at least one rule matches). 'all' is typical for ICP; 'any' is useful for exploratory scans.
industriesarray
Target industries. Free-form strings — typically SNI codes (Swedish industry classification) like '62.01' for software development, but can be arbitrary labels ('SaaS', 'fintech', 'healthtech'). Match is substring-insensitive.
regionsarray
Geographic regions. Countries, counties, cities — whatever granularity your research agent reports. Match is substring-insensitive.
min_employeesinteger
Minimum headcount (inclusive). Omit for no lower bound.
max_employeesinteger
Maximum headcount (inclusive). Omit for no upper bound.
min_revenue_centsinteger
Minimum annual revenue in minor units (cents/öre). Currency is implicit per-region (use a second ICP with a converted band if mixing currencies).
max_revenue_centsinteger
Maximum annual revenue in minor units.
required_signalsarray
Buying signals that must be present on the lead for a match. Free-form strings matched against lead.enrichment.signals (if the research agent populated them): 'hiring', 'funding_event', 'news_mention', 'procurement_posting', 'technology_stack_match', etc.
excluded_keywordsarray
Disqualifier strings. If any appears in the lead's company, name, or enrichment.notes, the lead is excluded regardless of other matches. Use for known bad fits ('student', 'sole trader', defunct brands).
target_rolesarray
Decision-maker roles to target. When specified, leads whose role field doesn't match any entry are excluded. Match is substring-insensitive ('VP Sales' matches 'VP of Sales'). Leave empty to match company-only leads (no person attached yet).
min_scoreinteger
Minimum agent-computed fit score (0-100) the lead must carry. Filters out low-quality research output before it reaches the pipeline.
notesstring
Free-form description of the target — who this ICP is for, why it exists.
source_urlstring
A saved LinkedIn search URL that already encodes the target filter. (Apollo, allabolag and other sources will activate when those integrations ship; for now LinkedIn is the only route that completes end-to-end — pasting a non-LinkedIn URL will fail at ingest time with a clear "add an io/… element" message.) When set, the ICP's sourcing phase fetches leads via the matching io/* element in the circle (io/linkedin for linkedin.com URLs). The structured firmographic/behavioral rules below still run as a post-qualification filter if populated — leave them empty to accept every sourced lead.

Capabilities

Inherited from modifiers
  • Evaluate
  • Observe

Operations

  • attachPOST
  • deleteDELETE
  • detachPOST
  • disablePOST
  • enablePOST
  • evaluatePOST
  • getGET
  • get_attached_modifiersGET
  • intentionGET
  • list_attachmentsGET
  • readme_updatePOST
  • schemaGET
  • updatePATCH

Ports

Inputs

  • match_modeconfig
  • industriesconfig
  • regionsconfig
  • min_employeesconfig
  • max_employeesconfig
  • min_revenue_centsconfig
  • max_revenue_centsconfig
  • required_signalsconfig
  • excluded_keywordsconfig
  • target_rolesconfig
  • min_scoreconfig
  • source_urlconfig

Composition

Attaches

Errors / when it fails

min_employees must be ≤ max_employees
Fails unless: min_employees <= max_employees
min_revenue_cents must be ≤ max_revenue_cents
Fails unless: min_revenue_cents <= max_revenue_cents

Validation rules

  • No source_url and no structured rules populated — this ICP has nothing to match or source against

ICP (icp)

Category: modifiers | Form: | Symbol: Ic

Ideal Customer Profile — declarative match rules that filter leads into pursuits

An ICP declares the shape of a company/contact worth pursuing. Two modes of definition, both consumed by sales-board.ingest-icp-matches: URL mode (source_url set) — paste a saved LinkedIn search URL that already encodes the filter server-side. ingest-icp-matches routes the URL through io/linkedin in the circle, which publishes people-vertical results as cards on the sales-board’s first column and persists company-vertical results into data/organizations. The URL IS the filter. Apollo, allabolag and other sources will activate as those io/* elements ship the ingest-search-url contract; for now LinkedIn is the only route that completes end-to-end. Rules mode (structured fields) — industries, regions, company size band, revenue band, buying signals, target roles, excluded keywords, min score. Rules are additive: every populated field narrows the match. A lead passes only if it satisfies ALL populated fields (AND), unless match_mode=‘any’ is set (OR). The modes compose: if both source_url AND structured rules are populated, URL-sourced leads are post-qualified against the rules before opening pursuits. Not a middleware — runs at sales-board op invocation time, not per-request. Multiple ICPs can coexist per circle (e.g. one for enterprise, one for SMB, one for a specific campaign). Sales-board selects which ICP to use by reference.

Guide

Ideal Customer Profile — declarative match rules that filter leads into pursuits.

What It Does

An ICP declares the shape of a company or contact worth pursuing. It is a purely declarative ruleset (a config modifier) — it has no operations of its own and never sits in a request/response middleware path. Instead, specific consumer ops (primarily sales-board.ingest-icp-matches) read the ICP’s spec by reference and filter raw research leads against its rules, promoting matches into Pursuits.

There are two ways to define an ICP, and they compose:

  • Rules mode (structured fields) — industries, regions, company-size band, revenue band, buying signals, target roles, excluded keywords, and a minimum fit score. Rules are additive: every populated field narrows the match. A lead passes only if it satisfies all populated fields (AND) unless match_mode is set to any (OR).
  • URL mode (source_url set) — a saved LinkedIn search URL that already encodes the filter server-side. The consumer routes the URL through the matching io/* element in the circle (LinkedIn for linkedin.com URLs); the URL is the filter. When both source_url and structured rules are populated, URL-sourced leads are post-qualified against the rules before pursuits open.

Evaluation is on_demand — the ruleset runs at consumer-op invocation time, not per-request. Multiple ICPs can coexist per circle (e.g. one for enterprise, one for SMB, one for a campaign); the consumer selects which ICP to use by reference.

Element Definition

  • Type: icp · Category: modifiers · Form: modifier (modifier_type: config)
  • Evaluation: on_demand (no middleware phase)
  • Applies to: * (attachable to any element that wants to reference match rules; the primary consumer is sales-board)

Properties

PropertyTypeDefaultNotes
match_modestring (all | any)allHow populated rules combine — AND vs OR.
industriesarray of string[]Target industries (SNI codes or labels like SaaS); substring-insensitive match.
regionsarray of string[]Geographic regions; substring-insensitive match.
min_employeesinteger (≥ 0)Minimum headcount, inclusive.
max_employeesinteger (≥ 0)Maximum headcount, inclusive.
min_revenue_centsinteger (≥ 0)Minimum annual revenue in minor units (cents/öre).
max_revenue_centsinteger (≥ 0)Maximum annual revenue in minor units.
required_signalsarray of string[]Buying signals that must be present (hiring, funding_event, …).
excluded_keywordsarray of string[]Disqualifier strings; any match excludes the lead.
target_rolesarray of string[]Decision-maker roles; substring-insensitive. Empty matches company-only leads.
min_scoreinteger (0–100)0Minimum agent-computed fit score the lead must carry.
notesstring (textarea)Free-form description of who this ICP is for.
source_urlstring (uri)Saved LinkedIn search URL; presence switches the ICP to URL sourcing mode.

No property is required (required: []).

Ports

None defined.

Capabilities

CapabilityDescription
lead-matchingEvaluate a lead against the ICP ruleset.
prospect-scanFilter a lead list down to ICP matches.

Error Codes

CodeClassRetryableMeaning
ICP_CONFIG_INVALIDvalidationnoICP configuration has conflicting or malformed rules.
ICP_NO_RULESvalidationnoICP has no populated rules — every lead would match trivially.

Operations

ICP exposes no element-specific operations (operations: {}). It is a pure config element, following the precedent set by modifier/variable. Inspect an ICP via the generic element GET endpoint, which returns the element’s spec (including the full rule set). The matching logic lives on the consumer side (e.g. sales-board.ingest-icp-matches), which reads ICP.spec by reference.

Quick Start

Create an ICP with structured rules (no element-specific invoke op — you configure its spec, then a consumer op references it):

# Create an enterprise-fintech ICP in the circle
POST /api/{circle}/enterprise-fintech-icp
{
  "spec": {
    "match_mode": "all",
    "industries": ["fintech", "SaaS"],
    "regions": ["Sweden", "Norway"],
    "min_employees": 50,
    "target_roles": ["VP Sales", "CTO"],
    "min_score": 70,
    "notes": "Nordic enterprise fintech buyers"
  }
}

# Inspect the ICP (returns the full rule set in its spec)
GET /api/{circle}/enterprise-fintech-icp

URL sourcing mode — paste a saved LinkedIn search URL instead:

POST /api/{circle}/linkedin-sourced-icp
{
  "spec": {
    "source_url": "https://www.linkedin.com/search/results/people/?keywords=...",
    "min_score": 60
  }
}

The ICP is then consumed by a sales-board op (ingest-icp-matches), which reads these rules by reference, sources/filters leads, and opens pursuits for matches.

Common Mistakes

  • Empty ICP matches everything. With no source_url and no structured rules populated, the ICP has nothing to match or source against — a validation warning fires (and conceptually maps to ICP_NO_RULES). Populate at least one rule or a source_url.
  • Inverted bands. If both min_employees and max_employees are set, min must be ≤ max; likewise for min_revenue_cents/max_revenue_cents. Inverted bands fail validation (min_employees must be ≤ max_employees).
  • Non-LinkedIn source_url. LinkedIn is currently the only route that completes end-to-end. Apollo, allabolag and other sources activate as those io/* integrations ship; pasting a non-LinkedIn URL fails at ingest time.
  • Revenue in major units. *_revenue_cents are minor units (cents/öre), not whole currency — 50_000_00 is 50,000, not 5,000,000.

Relationships

  • Attaches to: circle

Capabilities

  • lead-matching: Evaluate a lead against the ICP ruleset
  • prospect-scan: Filter a lead list down to ICP matches

Properties

PropertyTypeDefaultDescription
match_modestring"all"How populated fields combine. ‘all’ = AND (every rule must match), ‘any’ = OR (at least one rule matches). ‘all’ is typical for ICP; ‘any’ is useful for exploratory scans.
industriesarray[]Target industries. Free-form strings — typically SNI codes (Swedish industry classification) like ‘62.01’ for software development, but can be arbitrary labels (‘SaaS’, ‘fintech’, ‘healthtech’). Match is substring-insensitive.
regionsarray[]Geographic regions. Countries, counties, cities — whatever granularity your research agent reports. Match is substring-insensitive.
min_employeesintegerMinimum headcount (inclusive). Omit for no lower bound.
max_employeesintegerMaximum headcount (inclusive). Omit for no upper bound.
min_revenue_centsintegerMinimum annual revenue in minor units (cents/öre). Currency is implicit per-region (use a second ICP with a converted band if mixing currencies).
max_revenue_centsintegerMaximum annual revenue in minor units.
required_signalsarray[]Buying signals that must be present on the lead for a match. Free-form strings matched against lead.enrichment.signals (if the research agent populated them): ‘hiring’, ‘funding_event’, ‘news_mention’, ‘procurement_posting’, ‘technology_stack_match’, etc.
excluded_keywordsarray[]Disqualifier strings. If any appears in the lead’s company, name, or enrichment.notes, the lead is excluded regardless of other matches. Use for known bad fits (‘student’, ‘sole trader’, defunct brands).
target_rolesarray[]Decision-maker roles to target. When specified, leads whose role field doesn’t match any entry are excluded. Match is substring-insensitive (‘VP Sales’ matches ‘VP of Sales’). Leave empty to match company-only leads (no person attached yet).
min_scoreinteger0Minimum agent-computed fit score (0-100) the lead must carry. Filters out low-quality research output before it reaches the pipeline.
notesstringFree-form description of the target — who this ICP is for, why it exists.
source_urlstringA saved LinkedIn search URL that already encodes the target filter. (Apollo, allabolag and other sources will activate when those integrations ship; for now LinkedIn is the only route that completes end-to-end — pasting a non-LinkedIn URL will fail at ingest time with a clear “add an io/… element” message.) When set, the ICP’s sourcing phase fetches leads via the matching io/* element in the circle (io/linkedin for linkedin.com URLs). The structured firmographic/behavioral rules below still run as a post-qualification filter if populated — leave them empty to accept every sourced lead.

Operations

attach

Post /ops/attach | Auth: Read

Attach this modifier to a target element

Attaches this modifier to a target element. The target_id must be a UUID of an existing element that supports this modifier type (check applies_to in definition.yaml). Priority controls evaluation order when multiple modifiers of the same type are attached — lower priority runs first. The attachment is stored in element_modifiers table. Cascade resolution runs at bond-time to merge this modifier into the target’s resolved config. Common mistake: attaching to an incompatible element type — check topology rules first.

delete

Delete /ops/delete | Auth: Admin

Delete element (soft delete)

Soft delete — sets state to ‘deleted’ but retains the record. Cannot delete elements that have children (has_no_bond precondition) or active runs. Requires admin auth and confirmation.

detach

Post /ops/detach | Auth: Read

Detach this modifier from a target element

Removes this modifier from a target element. Requires the target_id. Pervasive modifiers (audit, policy) can only be detached at the level they were originally attached — inherited pervasive modifiers cannot be detached by child elements. After detach, cascade resolution re-runs to remove this modifier’s effect from the resolved config.

disable

Post /ops/disable | Auth: Admin

Disable element (hides and prevents use)

Idempotent — safe to call on already-disabled elements. Optionally pass a reason string. Disabled elements cannot be invoked or executed. Inverse of enable.

enable

Post /ops/enable | Auth: Admin

Enable element (makes usable and visible)

Idempotent — safe to call on already-enabled elements. Transitions element to ready/enabled state. Cannot enable deleted elements. Inverse of disable.

evaluate

Post /ops/evaluate | Auth: Read

Evaluate modifier against current context

Evaluates this modifier against a context and optional target_id. Returns applies (bool), result (modifier-specific), and message. For modifiers without custom evaluation logic, returns a default pass result. Auth-policy returns allowed/denied. This is the explicit evaluation endpoint — during normal request flow, modifiers are evaluated automatically by the cascade resolver as middleware.

get

Get /ops/get | Auth: Read

Get element details

Element is already resolved by the routing layer — this returns the cached element, not a fresh DB query. Use the path /api/{circle}/{slug} to address elements.

get_attached_modifiers

Get /ops/attached/{target_id} | Auth: Read

Get all modifiers attached to a target element

Lists all modifiers attached to a specific target element, including modifier_id, type, subcategory, and priority. Useful for debugging cascade resolution or understanding which policies apply to an element before invoking it.

intention

Get /ops/intention | Auth: Read

Get element intention with full inheritance chain

Returns three levels: direct (this element’s intention), inherited (from category and root), and resolved (final merged intention). Useful for understanding an element’s purpose in context of its hierarchy.

list_attachments

Get /ops/targets | Auth: Read

List all elements this modifier is attached to

Returns all target elements where this modifier is currently applied. Shows target_id, target_type, priority, and cascade_policy.

readme_update

Post /ops/readme_update | Auth: Write

Update element README.md content

Creates or overwrites README.md in the element’s git repo. Commits to the draft branch. Content must be provided as a markdown string.

schema

Get /ops/schema | Auth: Read

Get element input/output schema (MCP tools/list compatible)

Returns type-level port schemas from the TypeRegistry — not instance-specific overrides. Includes direction (input/output), required flag, and JSON schema per port. Useful for understanding what data an element accepts and produces.

update

Patch /ops/update | Auth: Write

Update element

Partial update — send only the fields you want to change. spec, name, and intention are all independently optional. spec MUST be a JSON object when present; deep-merged into the existing spec by default. Empty {"spec":{}} preserves existing spec content but still records a new version (no-op for content, not for version state). To clear/replace the entire spec wholesale send {"spec":{...},"deep":false}. List-typed spec fields use replace semantics (the patch list replaces the existing list, no array merging). Coordinates Git + DB writes. Slug cannot be changed after creation.

Error Codes

CodeClassRetryableDescription
ICP_CONFIG_INVALIDvalidationnoICP configuration has conflicting or malformed rules
ICP_NO_RULESvalidationnoICP has no populated rules — every lead would match trivially

Lifecycle / runtime

Inherited from modifiers

Execution model: async

Observability

Defined for this element

Metrics

  • evaluation_count
  • match_count
  • reject_count

Events

  • icp.evaluated
  • icp.matched
  • icp.rejected

Pricing / cost

Platform default

Operation costs

  • create: free
  • update: free
  • delete: free
  • get: free
  • list: free
  • invoke: 10000 micro-AU
  • tool_use: free

Set it up

LinkedIn search URLstring
Paste a LinkedIn search URL (Apollo, allabolag and others come later). If set, this alone defines the target — leave empty to use the structured rules below.
Who is this ICP for?string
Free-form description — who this ICP targets and why it exists
Match modestring
'all' (AND) = every populated rule must match. 'any' (OR) = at least one must match.
Industriesstring
Target industry codes or labels (SNI, SaaS, fintech, etc.)
Regionsstring
Geographic targets — countries, counties, cities
Min employeesstring
Minimum headcount
Max employeesstring
Maximum headcount
Min revenue (cents)string
Minimum annual revenue in minor units
Max revenue (cents)string
Maximum annual revenue in minor units
Required signalsstring
Buying signals the lead must carry (hiring, funding_event, etc.)
Target rolesstring
Decision-maker roles — VP Sales, CTO, Head of X
Excluded keywordsstring
Disqualifier strings — lead is excluded if any appear
Min fit scorestring
Minimum agent-computed score (0-100)