Skip to main content
connic
Build

Database Tools

The built-in persistent database stores documents in named collections. Tools insert, query, update, and delete data with filters, without database hosting, migrations, or schema setup.

Last updated

Database tools vs. Retrieval tools

connic provides two storage systems for agents. They serve different purposes and are often used together.

Database tools

Stores structured data and finds it by exact field values. Query with operators like $gt, $in, or $and against any field in any collection.

Supports exact lookups, field filters, counts, and record creation, updates, and deletion.

Retrieval tools

Stores text and finds it by meaning. A query for "cancellation rules" can surface a document titled "return and refund policy".

Ranks unstructured text such as documentation, FAQs, and notes by relevance.

Wrapper functions can expose names such as save_order and fetch_orders while fixing collection names and field mappings.

tools/order_tools.py
from connic.tools import db_insert, db_find, db_update, db_delete, db_count

async def save_order(
    order_id: str,
    customer_email: str,
    product: str,
    amount: float,
) -> dict:
    """Save a new order to the database."""
    return await db_insert("orders", {
        "order_id":       order_id,
        "customer_email": customer_email,
        "product":        product,
        "amount":         amount,
        "status":         "pending",
    })

async def fetch_orders(
    customer_email: str | None = None,
    status: str | None = None,
    limit: int = 20,
) -> list:
    """Fetch orders, optionally filtered by customer or status."""
    filter_dict = {}
    if customer_email:
        filter_dict["customer_email"] = customer_email
    if status:
        filter_dict["status"] = status

    result = await db_find(
        "orders",
        filter=filter_dict,
        sort={"amount": -1},
        limit=limit,
    )
    return result["documents"]

async def update_status(order_id: str, new_status: str) -> dict:
    """Update the status of a specific order."""
    return await db_update(
        "orders",
        filter={"order_id": order_id},
        update={"status": new_status},
    )

async def cancel_order(order_id: str) -> dict:
    """Cancel a pending order."""
    return await db_update(
        "orders",
        filter={"order_id": order_id, "status": "pending"},
        update={"status": "cancelled"},
    )

Use the custom tools in the agent YAML:

agents/order-processor.yaml
version: "1.0"

name: order-processor
model: connic/gpt-5.6-luna
description: "Manages orders using the built-in database"
system_prompt: |
  You are an order management assistant. Use your tools to
  create, look up, update, and delete orders.

tools:
  - order_tools.save_order
  - order_tools.fetch_orders
  - order_tools.update_status
  - order_tools.cancel_order

Direct use

For simple agents or prototyping, declare the database tools directly in the YAML and let the agent construct filters itself.

agents/db-agent.yaml
version: "1.0"

name: db-agent
model: connic/gpt-5.6-luna
description: "Works with project database records"
system_prompt: |
  Use database tools to read and update project records.

tools:
  - db_insert
  - db_find
  - db_update
  - db_upsert
  - db_delete
  - db_count
  - db_list_collections

Tool reference

db_find

Query documents using filters

How it works

Query documents from a collection using filters. Supports sorting, pagination, field projection, and distinct value enumeration.

tools/queries.py
result = await db_find("orders")
# Returns all orders (up to 100)

result = await db_find("orders", filter={"status": "pending"})
# Returns only pending orders
tools/queries.py
# Sort, paginate, and project
result = await db_find(
    "orders",
    filter={"amount": {"$gt": 100}},
    sort={"amount": -1},      # Highest amount first
    limit=20,
    skip=40,                  # Page 3 of 20
    fields=["order_id", "customer_email", "amount"],
)
documents = result["documents"]

# Nested field query
result = await db_find("users", filter={"address.city": "Berlin"})

# Multiple conditions
result = await db_find("orders", filter={
    "$and": [
        {"status": {"$in": ["paid", "shipped"]}},
        {"amount": {"$gte": 50}},
    ]
})

# Distinct values (ignores sort/limit/fields)
result = await db_find("orders", distinct="status")
statuses = result["values"]   # ["cancelled", "paid", "pending", "shipped"]

# Workshops within 25 km, nearest first
# (each stores "location": {"lat": 48.15, "lng": 11.58})
result = await db_find("workshops", filter={
    "type": "workshop",
    "location": {"$near": {"lat": 48.137, "lng": 11.575, "maxDistance": 25000}},
})
nearest = result["documents"][0]
nearest["_distance"]          # meters from the point

Parameters

ParameterTypeDefaultDescription
collectionstrrequiredName of the collection to query
filterdict{}Filter dict. See operators below.
sortdictNoneSort order. 1 = ASC, -1 = DESC. Use underscore prefix for system fields. e.g. {"_created_at": -1}
limitint100Max documents to return. Hard cap at 1000.
skipint0Documents to skip. Used for pagination.
fieldslistNoneField paths to include. Omit for full documents. e.g. ["id", "status", "amount"]
distinctstrNoneIf set, return unique values for this field. sort/limit/skip/fields are ignored.

Return value

Without distinct: {"documents": [...], "count": N}
With distinct: {"values": [...], "count": N}

Each document contains application data fields plus the system fields _id (UUID), _created_at, and _updated_at. System fields always use the underscore prefix, both when reading and when filtering or sorting.

With a $near filter, each document also has _distance in meters, and results come nearest first unless you set sort.

Filter operators

System fields use an underscore prefix: _id (UUID), _created_at, _updated_at. Use id (no underscore) for a user-defined field inside documents. Nested fields use dot notation (e.g. address.city).

OperatorMeaningExample
{field: value}Equality (shorthand){"status": "active"}
$eqEqual{"amount": {"$eq": 100}}
$neNot equal{"status": {"$ne": "cancelled"}}
$gtGreater than{"amount": {"$gt": 50}}
$gteGreater than or equal{"priority": {"$gte": 3}}
$ltLess than{"score": {"$lt": 0.5}}
$lteLess than or equal{"age": {"$lte": 30}}
$inValue in list{"status": {"$in": ["paid", "shipped"]}}
$ninValue not in list{"status": {"$nin": ["cancelled"]}}
$andAll conditions true{"$and": [{"a": 1}, {"b": 2}]}
$orAny condition true{"$or": [{"status": "new"}, {"urgent": true}]}
$notNegate condition{"$not": {"status": "inactive"}}
$norNone of the conditions true{"$nor": [{"status": "cancelled"}, {"flagged": true}]}
$exists: trueField exists{"email": {"$exists": true}}
$exists: falseField does not exist{"phone": {"$exists": false}}
$containsArray contains value{"tags": {"$contains": "urgent"}}
$elemMatchArray element matches conditions{"items": {"$elemMatch": {"sku": "A-100", "qty": {"$gte": 2}}}}
$regexMatches regex (case-insensitive){"name": {"$regex": "^John"}}
$nearWithin maxDistance meters of a point{"location": {"$near": {"lat": 48.137, "lng": 11.575, "maxDistance": 25000}}}

Multiple operators on one field combine with AND: {"article_id": {"$exists": true, "$nin": [...]}}. Negation operators ($ne, $nin) also match documents where the field is missing. Add "$exists": true to restrict to documents that have the field. $in and $nin compare values as text, so keep list values as strings. Each $in or $nin list accepts at most 1,000 values.

$near works on fields stored as {"lat": number, "lng": number} and measures great-circle distance in meters. maxDistance is optional. Use one $near per filter, at the top level. Documents without numeric coordinates never match.

db_insert

Insert one or more documents into a collection

How it works

Insert one or more documents into a collection. The collection (and the environment schema) are created automatically on first insert.

tools/inserts.py
# Insert one document
result = await db_insert("customers", {
    "name":   "Alice",
    "email":  "alice@example.com",
    "plan":   "pro",
})
# result["inserted"][0]["_id"]  -> auto-generated UUID
# result["inserted_count"]      -> 1
tools/inserts.py
# Insert multiple documents at once
result = await db_insert("events", [
    {"type": "login",    "user": "alice", "ts": "2026-01-01T10:00:00Z"},
    {"type": "purchase", "user": "alice", "amount": 49.99},
    {"type": "logout",   "user": "alice", "ts": "2026-01-01T10:45:00Z"},
])
# result["inserted_count"] -> 3

# Collection is created automatically on first insert
# No setup or schema definition needed

Parameters

ParameterTypeDefaultDescription
collectionstrrequiredCollection name. Must start with a letter; lowercase letters, digits, underscores only.
documentsdict | list[dict]requiredA single document or a list of documents. Each document can contain any fields.

Return value

{"inserted": [...], "inserted_count": N}
Each inserted doc in the list contains application data fields plus system fields _id (auto-generated UUID), _created_at, _updated_at.

db_update

Update documents matching a filter

How it works

Update all documents that match a filter. The update dict is merged into each matching document. Fields not mentioned in update are preserved.

tools/updates.py
# Update a specific document
result = await db_update(
    "orders",
    filter={"order_id": "ORD-001"},
    update={"status": "shipped"},
)
# result["updated_count"] -> 1
tools/updates.py
# Update multiple documents at once
result = await db_update(
    "orders",
    filter={"status": "pending", "amount": {"$gt": 500}},
    update={"status": "priority", "flagged": True},
)
# result["updated_ids"] -> ["uuid1", "uuid2", ...]
# result["updated_count"] -> N

# The update dict is MERGED into existing documents
# Fields not mentioned in update are kept as-is

Parameters

ParameterTypeDefaultDescription
collectionstrrequiredCollection name
filterdictrequiredFilter dict selecting which documents to update. Empty dict {} updates ALL documents.
updatedictrequiredPartial document with fields to set. Merged into existing documents. Set a field to null to remove it.

Return value

{"updated_ids": [...], "updated_count": N}

Filter operators

System fields use an underscore prefix: _id (UUID), _created_at, _updated_at. Use id (no underscore) for a user-defined field inside documents. Nested fields use dot notation (e.g. address.city).

OperatorMeaningExample
{field: value}Equality (shorthand){"status": "active"}
$eqEqual{"amount": {"$eq": 100}}
$neNot equal{"status": {"$ne": "cancelled"}}
$gtGreater than{"amount": {"$gt": 50}}
$gteGreater than or equal{"priority": {"$gte": 3}}
$ltLess than{"score": {"$lt": 0.5}}
$lteLess than or equal{"age": {"$lte": 30}}
$inValue in list{"status": {"$in": ["paid", "shipped"]}}
$ninValue not in list{"status": {"$nin": ["cancelled"]}}
$andAll conditions true{"$and": [{"a": 1}, {"b": 2}]}
$orAny condition true{"$or": [{"status": "new"}, {"urgent": true}]}
$notNegate condition{"$not": {"status": "inactive"}}
$norNone of the conditions true{"$nor": [{"status": "cancelled"}, {"flagged": true}]}
$exists: trueField exists{"email": {"$exists": true}}
$exists: falseField does not exist{"phone": {"$exists": false}}
$containsArray contains value{"tags": {"$contains": "urgent"}}
$elemMatchArray element matches conditions{"items": {"$elemMatch": {"sku": "A-100", "qty": {"$gte": 2}}}}
$regexMatches regex (case-insensitive){"name": {"$regex": "^John"}}
$nearWithin maxDistance meters of a point{"location": {"$near": {"lat": 48.137, "lng": 11.575, "maxDistance": 25000}}}

Multiple operators on one field combine with AND: {"article_id": {"$exists": true, "$nin": [...]}}. Negation operators ($ne, $nin) also match documents where the field is missing. Add "$exists": true to restrict to documents that have the field. $in and $nin compare values as text, so keep list values as strings. Each $in or $nin list accepts at most 1,000 values.

$near works on fields stored as {"lat": number, "lng": number} and measures great-circle distance in meters. maxDistance is optional. Use one $near per filter, at the top level. Documents without numeric coordinates never match.

db_upsert

Update a matching document or insert a new one

How it works

Update the first document matching filter, or insert a new one when nothing matches.

tools/upserts.py
# Create the order, or bump its status if it already exists.
result = await db_upsert(
    "orders",
    filter={"order_id": "ORD-001"},
    update={"status": "shipped"},
)
# result["operation"]  -> "inserted" or "updated"
# result["upserted_id"] -> "<uuid>"
tools/upserts.py
# insert_only fields apply ONLY when a new document is inserted.
# Use it for "first-seen" metadata that should not get overwritten
# on subsequent calls.
result = await db_upsert(
    "user_profiles",
    filter={"email": "alice@example.com"},
    update={"last_seen_at": "2026-05-12T08:00:00Z"},
    insert_only={"source": "signup_form", "created_by": "auth-worker"},
)

Parameters

ParameterTypeDefaultDescription
collectionstrrequiredCollection name. Auto-created on first write.
filterdictrequiredNon-empty filter identifying the target document, typically a natural key such as {"order_id": "ORD-001"}. Top-level equality keys are auto-copied into the document on insert; operator constructs ($gt, $in, $and, dot-notation) are not.
updatedictrequiredFields applied on both branches. On update, values merge and null removes a field. On insert, fields with null values are omitted.
insert_onlydictNoneFields written ONLY when a new document is inserted. Ignored on the update path.

Return value

{"upserted_id": "<uuid>", "operation": "inserted" | "updated"}

Filter operators

System fields use an underscore prefix: _id (UUID), _created_at, _updated_at. Use id (no underscore) for a user-defined field inside documents. Nested fields use dot notation (e.g. address.city).

OperatorMeaningExample
{field: value}Equality (shorthand){"status": "active"}
$eqEqual{"amount": {"$eq": 100}}
$neNot equal{"status": {"$ne": "cancelled"}}
$gtGreater than{"amount": {"$gt": 50}}
$gteGreater than or equal{"priority": {"$gte": 3}}
$ltLess than{"score": {"$lt": 0.5}}
$lteLess than or equal{"age": {"$lte": 30}}
$inValue in list{"status": {"$in": ["paid", "shipped"]}}
$ninValue not in list{"status": {"$nin": ["cancelled"]}}
$andAll conditions true{"$and": [{"a": 1}, {"b": 2}]}
$orAny condition true{"$or": [{"status": "new"}, {"urgent": true}]}
$notNegate condition{"$not": {"status": "inactive"}}
$norNone of the conditions true{"$nor": [{"status": "cancelled"}, {"flagged": true}]}
$exists: trueField exists{"email": {"$exists": true}}
$exists: falseField does not exist{"phone": {"$exists": false}}
$containsArray contains value{"tags": {"$contains": "urgent"}}
$elemMatchArray element matches conditions{"items": {"$elemMatch": {"sku": "A-100", "qty": {"$gte": 2}}}}
$regexMatches regex (case-insensitive){"name": {"$regex": "^John"}}
$nearWithin maxDistance meters of a point{"location": {"$near": {"lat": 48.137, "lng": 11.575, "maxDistance": 25000}}}

Multiple operators on one field combine with AND: {"article_id": {"$exists": true, "$nin": [...]}}. Negation operators ($ne, $nin) also match documents where the field is missing. Add "$exists": true to restrict to documents that have the field. $in and $nin compare values as text, so keep list values as strings. Each $in or $nin list accepts at most 1,000 values.

$near works on fields stored as {"lat": number, "lng": number} and measures great-circle distance in meters. maxDistance is optional. Use one $near per filter, at the top level. Documents without numeric coordinates never match.

db_delete

Delete documents matching a filter

How it works

Delete all documents that match a filter. A non-empty filter is required to prevent accidental full-collection deletion.

tools/deletes.py
# Delete a document by _id (UUID from db_insert/db_find)
result = await db_delete("orders", {"_id": "550e8400-e29b-41d4-a716-446655440000"})
# result["deleted_count"] -> 1

# Delete all documents in a collection
result = await db_delete("orders", {"_id": {"$exists": True}})

# Delete by a data field
result = await db_delete("orders", {"order_id": "ORD-001"})

# Delete with compound filter
result = await db_delete("logs", {
    "$and": [
        {"status": "archived"},
        {"_created_at": {"$lt": "2025-01-01"}},
    ]
})
# result["deleted_ids"] -> ["uuid1", ...]

Parameters

ParameterTypeDefaultDescription
collectionstrrequiredCollection name
filterdictrequiredFilter dict. Must not be empty. Use _id for system UUID. Use _id with $exists: true to delete all.

Return value

{"deleted_ids": [...], "deleted_count": N}

Filter operators

System fields use an underscore prefix: _id (UUID), _created_at, _updated_at. Use id (no underscore) for a user-defined field inside documents. Nested fields use dot notation (e.g. address.city).

OperatorMeaningExample
{field: value}Equality (shorthand){"status": "active"}
$eqEqual{"amount": {"$eq": 100}}
$neNot equal{"status": {"$ne": "cancelled"}}
$gtGreater than{"amount": {"$gt": 50}}
$gteGreater than or equal{"priority": {"$gte": 3}}
$ltLess than{"score": {"$lt": 0.5}}
$lteLess than or equal{"age": {"$lte": 30}}
$inValue in list{"status": {"$in": ["paid", "shipped"]}}
$ninValue not in list{"status": {"$nin": ["cancelled"]}}
$andAll conditions true{"$and": [{"a": 1}, {"b": 2}]}
$orAny condition true{"$or": [{"status": "new"}, {"urgent": true}]}
$notNegate condition{"$not": {"status": "inactive"}}
$norNone of the conditions true{"$nor": [{"status": "cancelled"}, {"flagged": true}]}
$exists: trueField exists{"email": {"$exists": true}}
$exists: falseField does not exist{"phone": {"$exists": false}}
$containsArray contains value{"tags": {"$contains": "urgent"}}
$elemMatchArray element matches conditions{"items": {"$elemMatch": {"sku": "A-100", "qty": {"$gte": 2}}}}
$regexMatches regex (case-insensitive){"name": {"$regex": "^John"}}
$nearWithin maxDistance meters of a point{"location": {"$near": {"lat": 48.137, "lng": 11.575, "maxDistance": 25000}}}

Multiple operators on one field combine with AND: {"article_id": {"$exists": true, "$nin": [...]}}. Negation operators ($ne, $nin) also match documents where the field is missing. Add "$exists": true to restrict to documents that have the field. $in and $nin compare values as text, so keep list values as strings. Each $in or $nin list accepts at most 1,000 values.

$near works on fields stored as {"lat": number, "lng": number} and measures great-circle distance in meters. maxDistance is optional. Use one $near per filter, at the top level. Documents without numeric coordinates never match.

db_count

Count documents in a collection

How it works

Count documents in a collection. Fast - does not load any documents.

tools/counts.py
# Count all documents
result = await db_count("orders")
total = result["count"]

# Count with filter
result = await db_count("orders", {"status": "active"})
active = result["count"]

# Useful for checking before paginating
result = await db_count("events", {"user": "alice"})
pages = (result["count"] + page_size - 1) // page_size

Parameters

ParameterTypeDefaultDescription
collectionstrrequiredCollection name
filterdict{}Optional filter. Counts all documents if omitted.

Return value

{"count": N}

Filter operators

System fields use an underscore prefix: _id (UUID), _created_at, _updated_at. Use id (no underscore) for a user-defined field inside documents. Nested fields use dot notation (e.g. address.city).

OperatorMeaningExample
{field: value}Equality (shorthand){"status": "active"}
$eqEqual{"amount": {"$eq": 100}}
$neNot equal{"status": {"$ne": "cancelled"}}
$gtGreater than{"amount": {"$gt": 50}}
$gteGreater than or equal{"priority": {"$gte": 3}}
$ltLess than{"score": {"$lt": 0.5}}
$lteLess than or equal{"age": {"$lte": 30}}
$inValue in list{"status": {"$in": ["paid", "shipped"]}}
$ninValue not in list{"status": {"$nin": ["cancelled"]}}
$andAll conditions true{"$and": [{"a": 1}, {"b": 2}]}
$orAny condition true{"$or": [{"status": "new"}, {"urgent": true}]}
$notNegate condition{"$not": {"status": "inactive"}}
$norNone of the conditions true{"$nor": [{"status": "cancelled"}, {"flagged": true}]}
$exists: trueField exists{"email": {"$exists": true}}
$exists: falseField does not exist{"phone": {"$exists": false}}
$containsArray contains value{"tags": {"$contains": "urgent"}}
$elemMatchArray element matches conditions{"items": {"$elemMatch": {"sku": "A-100", "qty": {"$gte": 2}}}}
$regexMatches regex (case-insensitive){"name": {"$regex": "^John"}}
$nearWithin maxDistance meters of a point{"location": {"$near": {"lat": 48.137, "lng": 11.575, "maxDistance": 25000}}}

Multiple operators on one field combine with AND: {"article_id": {"$exists": true, "$nin": [...]}}. Negation operators ($ne, $nin) also match documents where the field is missing. Add "$exists": true to restrict to documents that have the field. $in and $nin compare values as text, so keep list values as strings. Each $in or $nin list accepts at most 1,000 values.

$near works on fields stored as {"lat": number, "lng": number} and measures great-circle distance in meters. maxDistance is optional. Use one $near per filter, at the top level. Documents without numeric coordinates never match.

db_list_collections

List collections in the current environment

How it works

List all collections in the current environment with document counts and storage sizes.

tools/collections.py
result = await db_list_collections()

for col in result["collections"]:
    print(f"{col['name']}: {col['document_count']} docs, {col['size_bytes'] // 1024} KB")

# Result:
# orders: 1543 docs, 200 KB
# customers: 320 docs, 40 KB
# events: 8721 docs, 890 KB

Parameters

Takes no parameters.

Return value

{"collections": [...], "total": N}
Each collection has name, document_count, size_bytes.