Skip to main content

Shared resources

A resource is business data that your agent owns and that several jobs use: contacts, prompts, products, templates. When you declare a resource, Studio can list it, edit it and import it, and your forms can offer its items as choices. You write no user interface.

The Contacts resource of the Hello World agent in Studio

Let a coding agent do it

Install the Supervaizer integration skill in your coding agent, open your agent project and ask:

Make the data that several jobs share a Supervaize resource.

The skill asks you the design questions, then follows this guide and the Hello World example, with tests. The rest of this page is what it reads.

When to add one​

  • The same data is used by more than one job.
  • Operators must see or correct the data without a developer.
  • A form must let the operator choose an existing item.

Data that belongs to one job only is not a resource. Put it in the cases of the job, or on the Configure screen.

1. Declare the resource​

registration = build_v2_agent_registration(
agent_slug="my-agent",
display_name="My Agent",
a2ui_catalog_version="my-agent-ui.1",
surfaces=["job.start"],
actions=[
"job.start", "job.sync", "job.stop",
# A list does not change state: say so, and Studio lets viewers read it.
V2ActionDefinition(id="resource.contacts.list", mutating=False, scope="workspace"),
],
resources=[
{
"id": "contacts",
"label": "Contacts",
"scope": "workspace",
"auto_surface": True,
"operations": ["list", "create", "update", "delete", "import"],
"display": {
"title_field": "email",
"columns": ["first_name", "last_name", "email", "city"],
"search_fields": ["first_name", "last_name", "email", "city"],
},
"fields": [
{"id": "first_name", "label": "First name", "type": "string", "required": True},
{"id": "last_name", "label": "Last name", "type": "string"},
{"id": "email", "label": "Email", "type": "email"},
{"id": "city", "label": "City", "type": "string"},
],
}
],
)
KeyMeaning
idThe name of the resource in action ids and in forms.
scopeWho shares the data: workspace (all missions), mission or job.
operationsWhat Studio may do: list, get, create, update, delete, import.
auto_surfaceStudio builds the table and the forms from display and fields.
displayThe title field, the table columns and the search fields.
fieldsThe form fields, with the usual field definitions.
requires_contextThe context that a call needs. The default is ["workspace.id"].
mounted_viewsReplaces a generated view, such as edit or import, with a surface of your agent: {"view": "import", "surface": "..."}.

The SDK derives one action for each operation, named resource.<id>.<operation>, and the surface mission.agent.resource.<id>.

2. Handle the operations​

Register one handler for the derived actions, and return one effect for each operation.

CONTACT_ACTIONS = [f"resource.contacts.{op}" for op in ("list", "create", "update", "delete", "import")]

def handle_contacts(request: V2ActionRequest) -> dict:
workspace_id = request.workspace_authorization.workspace_id # verified by the SDK
store = contacts_of(workspace_id)
data = request.input
operation = request.action.rsplit(".", 1)[1]

if operation == "list":
items = store.all()
effect = {"type": "resource.listed", "count": len(items), "items": items}
elif operation == "create":
effect = {"type": "resource.created", "item": store.create(data)}
elif operation == "update":
item = store.update(data.get("id"), data)
if item is None:
return error("Contact not found")
effect = {"type": "resource.updated", "item": item}
elif operation == "delete":
if not store.delete(data.get("id")):
return error("Contact not found")
effect = {"type": "resource.deleted", "item": {"id": data.get("id")}}
else: # import
created = store.create_many(data.get("records") or [])
effect = {"type": "resource.imported", "count": len(created), "items": store.all(), "errors": []}

return {"status": "ok", "effects": [{"resource": "contacts", **effect}]}


def error(message: str) -> dict:
return {"status": "error", "effects": [{"type": "resource.action.failed", "resource": "contacts", "message": message}]}


for action in CONTACT_ACTIONS:
sv_server.register_v2_action(action, handle_contacts, agent_slug=agent.slug)

Each item needs a string id. Create the id on the agent side and ignore an id that comes with a create.

3. Use the resource in a form​

A resource_ref field lets the operator choose one item. Studio loads the choices with the list action.

{
"id": "contact_id",
"label": "Contact",
"type": "resource_ref",
"required": True,
"options_source": {"type": "resource", "resource": "contacts", "value_field": "id", "label_field": "email"},
}

Your action receives the id of the chosen item. Load the item from your store; do not trust other values from the form.

Keep the data of each workspace apart​

The same controller can serve several workspaces. A resource handler must never return the data of another workspace.

  • Take the workspace from request.workspace_authorization. The SDK verified it from a token that Studio signed.
  • Do not use a workspace id or slug from request.input. A caller can write any value there.
  • For a mission scope, filter on request.mission_id too.
  • request.mission, when Studio sends it, holds the mission id, name and metadata. Studio sends the current metadata on each request. Do not cache it. Use it to scope results, never as an access boundary. See the SDK guide.

Checklist​

  • The resource is in resources, with only the operations that the handler supports.
  • The list action is declared with mutating=False.
  • Every operation filters on the verified workspace.
  • Every operation validates its input and returns its effect.
  • Forms use resource_ref with options_source, not a free text field.

Reference​