Runtime Tools
Overview
Section titled “Overview”The upjack library provides 6 entity management operations. When using create_server(), these are automatically registered as 6 MCP tools per entity type (e.g., create_contact, get_contact, list_contacts). When using UpjackApp directly, they are available as Python methods.
The operations work on the entity definitions and schemas declared in the app’s upjack manifest. They are the primary interface through which agents (and custom code) interact with entity data.
v0.1 Implementation Notes
Section titled “v0.1 Implementation Notes”The current implementation uses in-memory file scanning for search (not a database index). Filters use flat key matching with comparison operators (not JSONPath). Errors are raised as Python exceptions (ValueError, FileNotFoundError, jsonschema.ValidationError). Singleton checking is not yet implemented. Immutable field protection is implemented — update operations silently strip id, type, version, created_at, and created_by from incoming data.
Tool Summary
Section titled “Tool Summary”| Tool | Purpose | Returns |
|---|---|---|
create_{entity} | Create a new entity | Created entity dict |
get_{entity} | Get a single entity by ID | Entity dict |
update_{entity} | Update an existing entity | Updated entity dict |
list_{plural} | Lightweight entity listing | Array of entity dicts |
search_{plural} | Text and structured search | Array of matching entity dicts |
delete_{entity} | Soft or hard delete an entity | Deleted entity dict |
MCP Tool Naming
Section titled “MCP Tool Naming”When using create_server(), tools are named per entity type:
| Entity Type | Plural | Tools Generated |
|---|---|---|
contact | contacts | create_contact, get_contact, update_contact, list_contacts, search_contacts, delete_contact |
deal | deals | create_deal, get_deal, update_deal, list_deals, search_deals, delete_deal |
create_entity
Section titled “create_entity”Create a new entity of a given type.
Python API
Section titled “Python API”app.create_entity(entity_type: str, data: dict, created_by: str = "agent") -> dictMCP Tool
Section titled “MCP Tool”create_{entity}(data: dict) -> dictParameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type | string | Yes | — | Entity type name (e.g., "contact"). |
data | object | Yes | — | Domain-specific fields for the entity. |
created_by | string | No | "agent" | Origin of the entity (user, agent, system, ingestion, schedule). |
Behavior
Section titled “Behavior”- Validate entity type: Confirm
entity_typeexists in the app’s entity definitions. RaiseValueErrorif not found. - Generate ID: Create a type-prefixed ULID using the entity’s
prefix(e.g.,ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X). - Inject base fields: Set
id,type,version(default 1),created_at(now),updated_at(now),created_by,status("active"). - Extract special fields: Pull
tags,relationships, andsourcefromdatainto base-level fields if present. - Merge data: Combine base fields with the remaining
datafields. - Validate against schema: If a schema is loaded for this entity type, validate the complete record. Raise
jsonschema.ValidationErroron failure — no entity is created. - Write file: Write the entity JSON to
{namespace}/data/{plural}/{id}.jsonwith 2-space indentation. - Return: The complete entity dict (base fields + app data merged flat).
Example
Section titled “Example”from upjack import UpjackApp
app = UpjackApp.from_manifest("manifest.json")
contact = app.create_entity("contact", { "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "new",})Returns:
{ "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "version": 1, "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T10:30:00Z", "created_by": "agent", "status": "active", "tags": [], "relationships": [], "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "new"}Errors
Section titled “Errors”| Condition | Exception | Message |
|---|---|---|
Unknown entity_type | ValueError | Unknown entity type 'foo'. Known types: ['contact', 'deal'] |
| Schema validation failure | jsonschema.ValidationError | Describes the specific validation error |
update_entity
Section titled “update_entity”Update an existing entity by ID.
Python API
Section titled “Python API”app.update_entity(entity_type: str, entity_id: str, data: dict, merge: bool = True) -> dictMCP Tool
Section titled “MCP Tool”update_{entity}(entity_id: str, data: dict) -> dictParameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type | string | Yes | — | Entity type name. |
entity_id | string | Yes | — | Entity ID (e.g., "ct_01HZ...5X"). |
data | object | Yes | — | Fields to update. |
merge | boolean | No | true | Whether to merge with existing data or replace. |
Behavior
Section titled “Behavior”- Load entity: Read the entity JSON file from its storage path. Raise
FileNotFoundErrorif not found. - Strip immutable fields: Silently remove
id,type,version,created_at, andcreated_byfrom incomingdata— these cannot be changed after creation. - Merge or replace: If
merge: true(default), shallow-mergedatainto the existing entity. Existing fields not present indataare preserved. Ifmerge: false, replace all fields except immutable base fields. - Auto-update timestamp: Set
updated_atto the current timestamp. - Validate against schema: If a schema is loaded, validate the merged entity. Raise
jsonschema.ValidationErroron failure — no changes are written. - Write file: Overwrite the entity JSON file.
- Return: The complete updated entity dict.
Example
Section titled “Example”updated = app.update_entity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", { "stage": "qualified", "score": 85,})Returns:
{ "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "version": 1, "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T14:22:00Z", "created_by": "agent", "status": "active", "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "qualified", "score": 85}Errors
Section titled “Errors”| Condition | Exception | Message |
|---|---|---|
| Entity not found | FileNotFoundError | Entity not found: ct_01HZ...5X |
| Schema validation failure | jsonschema.ValidationError | Describes the specific validation error |
entity_search
Section titled “entity_search”Text and structured search across entities of a given type.
Python API
Section titled “Python API”app.search_entities( entity_type: str, query: str | None = None, filter: dict | None = None, sort: str = "-updated_at", limit: int = 20,) -> list[dict]MCP Tool
Section titled “MCP Tool”search_{plural}(query: str | None, filter: dict | None, sort: str, limit: int) -> list[dict]Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type | string | Yes | — | Entity type to search. |
query | string | No | — | Case-insensitive substring match across all string-valued fields. |
filter | object | No | — | Structured filter with comparison operators. |
sort | string | No | "-updated_at" | Sort field. Prefix with - for descending. |
limit | integer | No | 20 | Maximum results to return. |
Behavior
Section titled “Behavior”- Load all entities: Read every JSON file in
{namespace}/data/{plural}/. Corrupt files are silently skipped. - Exclude deleted by default: Unless the filter explicitly includes a
statuskey, entities withstatus: "deleted"are excluded. - Apply text query: If
queryis provided, match case-insensitively against all string-valued fields in the entity. An entity matches if any string field contains the query as a substring. - Apply structured filters: If
filteris provided, evaluate each key-value pair against the entity. All conditions must match (AND logic). - Sort: Apply the sort expression. Default is
-updated_at(most recently updated first). - Limit: Return at most
limitresults. - Return: Array of complete entity dicts matching the query.
Filter Syntax
Section titled “Filter Syntax”Filters use flat field names (not JSONPath) with optional comparison operators:
# Direct equalityapp.search_entities("contact", filter={"stage": "qualified"})
# Comparison operatorsapp.search_entities("contact", filter={"score": {"$gte": 70}})
# Combined filters (AND logic)app.search_entities("contact", filter={ "stage": {"$in": ["qualified", "contacted"]}, "score": {"$gte": 60},})Supported operators:
| Operator | Meaning | Example |
|---|---|---|
| (direct value) | Equality | {"stage": "qualified"} |
$gt | Greater than | {"score": {"$gt": 50}} |
$gte | Greater than or equal | {"score": {"$gte": 70}} |
$lt | Less than | {"score": {"$lt": 50}} |
$lte | Less than or equal | {"score": {"$lte": 30}} |
$ne | Not equal | {"stage": {"$ne": "lost"}} |
$in | Value in array | {"stage": {"$in": ["qualified", "contacted"]}} |
$contains | Array field contains value | {"tags": {"$contains": "saas"}} |
$exists | Field exists (true) or is absent (false) | {"score": {"$exists": true}} |
Example
Section titled “Example”results = app.search_entities( "contact", query="TechCorp", filter={"score": {"$gte": 60}}, sort="-score", limit=10,)Returns:
[ { "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "version": 1, "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T14:22:00Z", "status": "active", "name": "Alice Chen", "email": "alice@example.com", "company_name": "TechCorp", "title": "VP Engineering", "stage": "qualified", "score": 85 }]Performance Note
Section titled “Performance Note”The v0.1 implementation loads all entity files into memory for each search operation. This works well for typical app sizes (hundreds to low thousands of entities). For larger datasets, a future version may introduce indexing.
entity_list
Section titled “entity_list”Lightweight entity listing. Simpler than entity_search — filters by status only.
Python API
Section titled “Python API”app.list_entities(entity_type: str, status: str = "active", limit: int = 50) -> list[dict]MCP Tool
Section titled “MCP Tool”list_{plural}(status: str, limit: int) -> list[dict]Parameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type | string | Yes | — | Entity type to list. |
status | string | No | "active" | Filter by status. |
limit | integer | No | 50 | Maximum results to return. |
Behavior
Section titled “Behavior”- Scan directory: List JSON files in
{namespace}/data/{plural}/, sorted byupdated_atin reverse order (most recently updated first). - Filter by status: Read each entity and include only those matching the
statusfilter. Default isactiveonly. - Limit: Return at most
limitresults. - Skip corrupt files: Files that fail JSON parsing are silently skipped.
- Return: Array of complete entity dicts matching the filter.
Example
Section titled “Example”contacts = app.list_entities("contact", status="active", limit=5)Returns:
[ { "id": "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", "type": "contact", "status": "active", "created_at": "2026-02-15T10:30:00Z", "updated_at": "2026-02-15T14:22:00Z", "name": "Alice Chen", "email": "alice@example.com", "stage": "qualified", "score": 85 }, { "id": "ct_01HZ3QLCN8XWVK1QPGB7NU9D6Y", "type": "contact", "status": "active", "created_at": "2026-02-14T09:15:00Z", "updated_at": "2026-02-14T16:45:00Z", "name": "Bob Martinez", "email": "bob@startup.io", "stage": "contacted", "score": 62 }]Difference from entity_search
Section titled “Difference from entity_search”| Aspect | entity_search | entity_list |
|---|---|---|
| Text search | Yes (substring match) | No |
| Structured filters | Yes (comparison operators) | Status only |
| Sort | Any field | By updated_at (most recent first) |
| Default limit | 20 | 50 |
| Returns | Full entity dicts | Full entity dicts |
| Use case | Finding specific entities | Browsing/overview |
entity_delete
Section titled “entity_delete”Delete an entity by ID. Soft delete (status change) by default; hard delete (file removal) with explicit flag.
Python API
Section titled “Python API”app.delete_entity(entity_type: str, entity_id: str, hard: bool = False) -> dictMCP Tool
Section titled “MCP Tool”delete_{entity}(entity_id: str, hard: bool = False) -> dictParameters
Section titled “Parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
entity_type | string | Yes | — | Entity type name. |
entity_id | string | Yes | — | Entity ID to delete. |
hard | boolean | No | false | If true, permanently remove the file. If false, set status to "deleted". |
Behavior
Section titled “Behavior”Soft Delete (default, hard: false)
Section titled “Soft Delete (default, hard: false)”- Load entity: Read the entity JSON file. Raise
FileNotFoundErrorif not found. - Set status: Change
statusto"deleted". - Update timestamp: Set
updated_atto current time. - Write file: Overwrite the entity JSON file.
- Return: The deleted entity dict (with updated status and timestamp).
Hard Delete (hard: true)
Section titled “Hard Delete (hard: true)”- Load entity: Read the entity JSON file. Raise
FileNotFoundErrorif not found. - Remove file: Delete the JSON file from the workspace.
- Return: The entity dict as it was before deletion.
Example
Section titled “Example”# Soft delete (default)result = app.delete_entity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X")assert result["status"] == "deleted"
# Hard delete (permanent)result = app.delete_entity("contact", "ct_01HZ3QKBN9YWVJ0RPFA7MT8C5X", hard=True)# File is removed from diskErrors
Section titled “Errors”| Condition | Exception | Message |
|---|---|---|
| Entity not found | FileNotFoundError | Entity not found: ct_01HZ...5X |
Common Errors
Section titled “Common Errors”All operations raise standard Python exceptions:
| Exception | When |
|---|---|
ValueError | Unknown entity type, invalid ID prefix |
FileNotFoundError | Entity ID does not exist on disk |
jsonschema.ValidationError | Data fails JSON Schema validation |
json.JSONDecodeError | Entity file contains corrupt JSON (raised by get_entity; list and search skip corrupt files silently) |
Concurrency and Consistency
Section titled “Concurrency and Consistency”Entity operations work on individual JSON files in the workspace. The consistency model is:
- Single-writer: The agent processes one tool call at a time. Concurrent writes to the same entity are not expected in v0.1.
- Read-your-writes: After an operation returns, subsequent reads will see the updated state.
- No transactions: Multi-entity operations are not atomic. If a skill needs to create a contact and an activity, they are two separate writes. This is acceptable for v0.1 where the agent is the sole writer.
Future Enhancements
Section titled “Future Enhancements”The following features are specified but not yet implemented in v0.1:
- Singleton constraint: Preventing creation of duplicate singleton entities.
- Indexed search: SQLite FTS or similar for faster search on large datasets.
- Structured error objects: MCP-level error responses with
code/message/detailsformat. - Entity summaries: Returning lightweight projections from
entity_listinstead of full objects.