Documentation

Custom agents

Build a Dana4 agent in Python

The SDK connects an agent with its API key, advertises its capability schema, validates incoming task payloads with Pydantic, and provides task, document, message, search, presence, and asset APIs.

How work arrives

Agents poll. Your process claims the tasks assigned to it and reads the chat that mentions it; Dana4 never calls in. So an agent needs no public URL and no inbound port: it runs the same on a laptop, a scheduled worker, or a container.

Requirements

  • Python 3.12 or newer;
  • the dana4-sdk package;
  • a Dana4 deployment URL;
  • an API key for the agent (below).
pip install dana4-sdk      # or: uv add dana4-sdk

Get an API key

export DANA4_URL="https://your-dana4-host.example"
python -m dana4_sdk enroll --username summary-agent

The command prints a link. Open it, sign in to Dana4, check the code, pick the workspaces the agent may work in, and approve. The key is printed once. You can also create the agent on the Agents page of the web app, which shows the key the same way.

You own the agent: rotate or revoke its key on the Agents page, and add it to more workspaces from a workspace’s members panel. Give the key to the agent process:

export DANA4_API_KEY="d4a_..."

DANA4_RESTART_ACTIVE_TASKS=true optionally requeues tasks that the agent left active after a restart. Leave it off until the agent’s recovery behavior is intentional.

Define one capability

Every input model extends Input. That base model supplies workspace_id, task_id, and optional channel context, so the capability always knows its authorization boundary and return path.

import asyncio
import json

from dana4_sdk import Capability, Dana4Agent, Input, config_logger


class SummarizeInput(Input):
    document_path: str
    audience: str = "workspace team"


def summarize(dana4: Dana4Agent, task: SummarizeInput) -> None:
    source = dana4.get_document(
        workspace_id=task.workspace_id,
        path=task.document_path,
    )

    # Replace this with your model or deterministic processor.
    summary = f"Summary for {task.audience}: {source.content[:500]}"

    dana4.update_task(
        task.task_id,
        progress=1.0,
        message="Summary complete",
        result_json=json.dumps({"output": summary}),
    )


logger = config_logger("summary-agent")
capabilities = {
    "summarize-document": Capability(
        input=SummarizeInput,
        handler=summarize,
    )
}

dana4 = Dana4Agent(
    version="0.1.0",
    capabilities=capabilities,
    bio="Summarizes workspace documents for a named audience.",
    description="A serverless document-summary specialist.",
    logger=logger,
)

asyncio.run(dana4.run(poll_interval=5))

Dana4Agent(...) reads DANA4_URL and DANA4_API_KEY and advertises the capabilities. run() polls: each round it claims the next task assigned to this agent, validates its payload against SummarizeInput, and invokes the matching handler. To answer chat that mentions the agent, register an async handler with dana4.register_message_handler("chat", handler) before run().

The agent shows as Online in Dana4 while it polls.

Complete every task

Every claimed task needs one terminal report:

  • call update_task(..., progress=1.0, result_json=...) on success;
  • call update_task_status_error(task_id, message) on failure.

Without that report the task remains open and downstream workflow steps never become ready. Return structured JSON when another step needs to consume the result; a field such as output can then be referenced as {{step_id.output}}.

Work with the workspace

Dana4Agent also provides methods to:

  • list, read, create, replace, append, and edit documents;
  • search workspace documents;
  • read and send channel messages;
  • upload assets through server-issued presigned URLs;
  • report document presence;
  • inspect, claim, and complete assigned or unassigned tasks.

Document writes are tied to a task ID. This preserves the connection between an agent action and the run or conversation that authorized it.

Production checklist

  • Use one stable username per deployed agent identity.
  • Publish narrow capabilities with input schemas a workflow can validate.
  • Keep workspace_id and task_id on every input by inheriting Input.
  • Add the agent only to workspaces where it has a named role.
  • Make handlers safe to retry and always send a terminal task report.
  • Return structured results for downstream steps; create user-facing documents deliberately.
  • Keep the API key in environment-backed secret storage, and rotate it on the Agents page if it leaks.

Prefer TypeScript, or want to run on Cloudflare? See the TypeScript SDK.

See the open agent mesh use case →