# Agentic AI and Spatial9: Integrations for LangGraph, CrewAI, A2A and More

> How to connect Spatial9 to LangGraph, CrewAI, Google ADK, Databricks, Snowflake, Vertex AI and the Spatial9 A2A agent, with code, diagrams and six real workflows.

Web page: https://spatial9.ai/blog/agentic-ai-platforms-a2a-spatial9-integrations
Author: Spatial9 Engineering Team
Published: 2026-10-25. Updated: 2026-10-25.

## Key takeaways
- Any agent platform that can call a remote MCP server or send an HTTP request can use the 37 Spatial9 tools at spatial9.ai/mcp.
- LangGraph, CrewAI, AutoGen and AG2, the OpenAI Agents SDK and Google ADK connect natively over MCP; n8n, Zapier and RPA tools use a single JSON-RPC POST.
- MCP connects an agent to tools, A2A connects agents to agents. Spatial9 runs its own A2A agent at spatial9.ai/a2a with five skills, live streaming, push notification webhooks and polling.
- Data platforms run the same loop on a schedule: Databricks Jobs, Snowflake Tasks, Vertex AI Agent Engine or Step Functions read a render queue table, start up to three renders and write the file links back.
- Every integration follows one contract: upload, start with an idempotency key, poll get_job_status every 30 to 60 seconds, then collect the files, with a human approving anything public.

Short answer: Spatial9 plugs into the agent world through one open door: the Spatial9 MCP server at https://spatial9.ai/mcp. Any agent platform that can call a remote MCP server, or simply send an HTTP request, can split stems, place instruments in 3D, render Eclipsa Audio with a headphone version, remix and publish to YouTube. Frameworks such as LangGraph, CrewAI, AutoGen, the OpenAI Agents SDK and Google ADK connect natively. Enterprise studios such as Agentforce, Copilot Studio and Vertex AI can reach it as a tool, and A2A clients can hand work to the Spatial9 A2A agent at https://spatial9.ai/a2a. This guide shows every route with working code.

[Image: Agents work best as a team. One plans, one mixes, one checks, one ships, and the sound orb travels between them.]

Our first agentic article, Agentic AI Meets Spatial Audio, explained what the Spatial9 MCP server does and how to connect ChatGPT, Claude or Gemini in two minutes. This one is for the people building agents rather than chatting with them: platform teams, automation engineers and developers who need to know exactly how Spatial9 fits into LangGraph, CrewAI, Agentforce, n8n or a multi-agent system that talks over A2A.

We name the tools, show the code and are careful about one distinction. Everything about the Spatial9 server in this article is a shipped feature. Everything about third-party platforms describes how those platforms generally work; their exact menus and plans change often, so check each vendor's own documentation before you build.


## At a glance

| Question | Answer |
| Spatial9 address | https://spatial9.ai/mcp, or https://spatial9.ai/mcp/free with no sign-in |
| Protocol | MCP over HTTP: JSON-RPC 2.0 with plain JSON replies, batches of 1 to 20 messages |
| Sign-in | OAuth 2.1 with PKCE (scope spatial_audio) or a personal s9_ key sent as a Bearer token |
| Tools | 37, from create_spatial_audio to publish_to_youtube |
| Native MCP routes | LangGraph, CrewAI, AutoGen and AG2, OpenAI Agents SDK, Google ADK, Goose and MCP-capable studios |
| HTTP routes | n8n, Zapier, Make-style builders, UiPath, RPA tools and any backend job |
| Data platforms | Databricks, Snowflake, BigQuery with Vertex AI, AWS, Microsoft Fabric and Dataiku run a scheduled loop over a render queue table |
| A2A route | The Spatial9 A2A agent at https://spatial9.ai/a2a, Agent Card at https://spatial9.ai/.well-known/agent-card.json |
| Job model | Asynchronous: start once, poll get_job_status every 30 to 60 seconds, then get_downloads |
| Typical times | 10 to 20 minutes per song, a few minutes per short video, 4 to 8 minutes per stem split |
| Limits | 3 renders at once, 20 per 24 hours, 5 YouTube publishes per 24 hours |


## MCP and A2A: two protocols, two jobs

Two open protocols now carry most agent traffic, and they solve different problems.

The Model Context Protocol (MCP) connects an agent to tools. A server lists its tools with typed inputs, and the agent calls them. Spatial9 is an MCP server: its tools are the studio.

The Agent2Agent protocol (A2A) connects agents to other agents. Google introduced it in April 2025 and contributed it to the Linux Foundation, where it is developed in the open with many vendors. An A2A agent publishes an Agent Card that describes its skills, receives tasks over JSON-RPC, reports task states such as working or input-required, and streams progress. IBM's Agent Communication Protocol (ACP), which came out of the BeeAI project, has since joined the A2A effort, so for new work A2A is the protocol to target.

[Image: A2A carries the request between agents. MCP carries the tool calls from your agent to the Spatial9 studio.]

The practical rule is simple:

- If your agent should use Spatial9, connect it to the MCP server.
- If another team's agent should ask a spatial audio specialist for help, send it to the Spatial9 A2A agent at https://spatial9.ai/a2a, or build your own specialist with the Spatial9 MCP tools and expose it over A2A.

The Spatial9 A2A agent covers the core jobs: immersive renders, stem splits, Dolby and ADM conversion, chorus previews and upload links. Remixing and YouTube publishing stay on the MCP server, so a custom specialist, about twenty lines of Python with Google ADK, is the route when you need those over A2A.

|  | MCP | A2A |
| Connects | An agent to tools and data | An agent to another agent |
| Discovery | tools/list on the server | An Agent Card at a well-known address |
| Unit of work | A tool call with typed arguments | A task with a lifecycle and artifacts |
| Long jobs | The tool returns a job ID, the agent polls | Task states and streamed status updates |
| Spatial9 role | Spatial9 is the server | Your agent wraps Spatial9 and becomes the peer |


## The agent platform landscape, and where Spatial9 fits

The agent market has split into six groups. Each one reaches Spatial9 by a slightly different road.

[Image: Six categories, one studio behind them. The badge on each card is the usual route to Spatial9.]


### Enterprise agent studios

Microsoft Copilot Studio, Salesforce Agentforce, Amazon Bedrock AgentCore, Google Vertex AI Agent Builder, ServiceNow AI Agents, Oracle AI Agent Studio and IBM watsonx Orchestrate are where large companies build governed agents. Most of them now accept remote MCP servers as tools, and several have announced A2A support for talking to agents built elsewhere. For a media company, the route is either to register spatial9.ai/mcp as a tool with a service key, or to call a dedicated spatial audio agent over A2A so the studio never handles audio files directly.


### No-code and low-code builders

Relevance AI, Gumloop, Dify, Botpress, Stack AI, Flowise, MindStudio, Vellum and Lindy let non-developers assemble agents from blocks. Where a builder offers an MCP block, point it at the Spatial9 address with a personal key. Where it offers only an HTTP request block, send the JSON-RPC call shown later in this article; it is a single POST.


### Developer frameworks

LangGraph, CrewAI, Microsoft AutoGen and its community fork AG2, the OpenAI Agents SDK (the production successor to the experimental Swarm project), Google's Agent Development Kit (ADK) and Block's Goose all ship MCP clients. This is the richest route: the agent sees all 37 tools with their full descriptions and can plan with them.


### Workflow automation

n8n, Zapier, UiPath, Automation Anywhere and Pega run deterministic flows with AI steps inside. n8n has an MCP client node; Zapier, UiPath and the RPA suites can always send an HTTP request. Use them when the trigger is an event, such as a new file in a folder or a ticket moving to "ready for mastering".


### Data and knowledge platforms

Palantir AIP, Databricks, Snowflake, Dataiku and Glean hold the catalog: track metadata, rights, release dates and stems locations. They are where a label decides which songs to convert. A scheduled notebook or pipeline then calls Spatial9 in batches, within the 3-at-once and 20-per-day limits. The data platform section below has working code for each of them.


### Connect and govern

Composio and Pipedream Connect manage tool connections and credentials for many users at once. Agentic Fabriq and CloudEagle.ai track which agents and apps have access to what. Zenity and Credo AI apply security and AI governance policy, and Galileo evaluates and observes agent behaviour. None of them render audio; all of them make an agent that does render audio safer to run at scale.

[Image: A planning session. Each agent brings a different skill, and the shared sphere is the mix they are working on.]


## The one contract every integration follows

Whatever the platform, a Spatial9 job has the same four steps. Getting these right matters more than the choice of framework.

[Image: The async job contract. Renders take minutes, so agents start once, poll calmly and collect the files.]

- Upload. If the audio is not already at a public link, call create_upload_link with a filename and media_type (music, video or image). It returns an upload_id and a direct_upload_url. PUT the file there; files up to 2 GB are accepted and the link is valid for 2 hours.
- Start. Call create_spatial_audio with upload_id or source_url, plus media_type, title and an idempotency_key. The key means a retried call never starts, or charges for, a second render.
- Poll. Call get_job_status with the job_id every 30 to 60 seconds. It reports the stage (separating, placing, encoding, rendering_binaural and so on) and a progress percentage.
- Collect. When the status is completed, call get_downloads. Music renders return the Eclipsa Audio file (iamf, 16 channels of third-order Ambisonics), Eclipsa Audio in MP4, a YouTube-ready MP4 and a binaural headphone WAV, plus the stage player link.

Call get_capabilities first in any new session. It returns the current formats, options and limits, so the agent never has to guess.


### The raw call, for any HTTP client

Every route in this article reduces to this request. It is the version to paste into an n8n HTTP node, a Zapier webhook step or a UiPath HTTP activity.

```bash
curl -s https://spatial9.ai/mcp \
  -H "Authorization: Bearer $SPATIAL9_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "create_spatial_audio",
      "arguments": {
        "source_url": "https://example.com/masters/night-drive.wav",
        "media_type": "music",
        "title": "Night Drive",
        "style": "live_concert",
        "idempotency_key": "catalog-2026-night-drive"
      }
    }
  }'
```

The reply carries a result.structuredContent object with job_id, status, stage and progress. A failed tool call returns result.isError: true with a plain-language explanation the agent can pass on.


### A polling helper in plain Python

Frameworks hide the protocol, but batch jobs often do not need a framework at all.

```python
import os, time, requests

MCP = "https://spatial9.ai/mcp"
HEADERS = {
    "Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}",
    "Accept": "application/json, text/event-stream",
}

def call(tool: str, args: dict) -> dict:
    body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
            "params": {"name": tool, "arguments": args}}
    r = requests.post(MCP, json=body, headers=HEADERS, timeout=60)
    r.raise_for_status()
    result = r.json()["result"]
    if result.get("isError"):
        raise RuntimeError(result["content"][0]["text"])
    return result.get("structuredContent", {})

def render(url: str, title: str) -> dict:
    job = call("create_spatial_audio", {
        "source_url": url, "media_type": "music", "title": title,
        "idempotency_key": f"batch-{title.lower().replace(' ', '-')}",
    })
    while job["status"] not in ("completed", "failed", "cancelled"):
        time.sleep(45)
        job = call("get_job_status", {"job_id": job["job_id"]})
        print(title, job["stage"], job.get("progress"))
    if job["status"] != "completed":
        raise RuntimeError(f"{title}: {job['status']}")
    return call("get_downloads", {"job_id": job["job_id"]})
```

If the process crashes and runs again, the same idempotency_key returns the original job instead of starting a new one.


## Framework integrations, with code

Each example below gives an agent the Spatial9 tools and a goal. The model plans the calls; your code only wires the connection. Keep the key in an environment variable or a secret store, never in the prompt.


### LangGraph

LangGraph builds agents as graphs with checkpoints, which suits long renders: the graph can pause while a job runs and resume later. The langchain-mcp-adapters package turns the Spatial9 tools into LangChain tools.

```python
import asyncio, os
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent

async def main():
    client = MultiServerMCPClient({
        "spatial9": {
            "transport": "streamable_http",
            "url": "https://spatial9.ai/mcp",
            "headers": {"Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}"},
        }
    })
    tools = await client.get_tools()
    agent = create_react_agent("openai:gpt-4.1", tools)
    result = await agent.ainvoke({"messages": [(
        "user",
        "Analyze job 'JOB_ID', then preview a remix that circles the guitar "
        "around the listener in the chorus. Do not render until I approve.",
    )]})
    print(result["messages"][-1].content)

asyncio.run(main())
```

For production, add a checkpointer and an interrupt before any node that calls remix_render without preview: true or publish_to_youtube, so a person approves those steps.


### CrewAI

CrewAI organises agents as a crew with roles. MCPServerAdapter from crewai-tools opens the connection and hands the tools to the crew for as long as the block runs.

```python
import os
from crewai import Agent, Task, Crew
from crewai_tools import MCPServerAdapter

spatial9 = {
    "url": "https://spatial9.ai/mcp",
    "transport": "streamable-http",
    "headers": {"Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}"},
}

with MCPServerAdapter(spatial9) as tools:
    pick = lambda *names: [t for t in tools if t.name in names]

    analyst = Agent(
        role="Music analyst",
        goal="Describe tempo, key, sections and instruments of each song",
        backstory="A producer who listens before touching the mix.",
        tools=pick("get_capabilities", "analyze_song"),
    )
    mixer = Agent(
        role="Spatial mixer",
        goal="Render Eclipsa Audio that suits the song's genre and energy",
        backstory="An immersive audio engineer who works on Spatial9.",
        tools=pick("create_spatial_audio", "get_job_status", "get_downloads"),
    )

    crew = Crew(
        agents=[analyst, mixer],
        tasks=[
            Task(description="Render https://example.com/masters/night-drive.wav "
                             "as music titled 'Night Drive' with idempotency key "
                             "'crew-night-drive'. Poll until completed.",
                 expected_output="The job_id and every download link",
                 agent=mixer),
            Task(description="Analyze the finished render and suggest one "
                             "movement idea for the chorus.",
                 expected_output="A short mix note", agent=analyst),
        ],
    )
    print(crew.kickoff())
```

Giving each agent only the tools it needs keeps plans short and stops the analyst from starting renders.


### OpenAI Agents SDK

The OpenAI Agents SDK is the supported, production-ready successor to Swarm. It keeps Swarm's handoff idea and adds tracing, guardrails and MCP support through MCPServerStreamableHttp.

```python
import asyncio, os
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp

async def main():
    async with MCPServerStreamableHttp(
        name="Spatial9",
        params={
            "url": "https://spatial9.ai/mcp",
            "headers": {"Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}"},
            "timeout": 60,
        },
        cache_tools_list=True,
    ) as spatial9:
        mixer = Agent(
            name="Spatial mixer",
            instructions="Use Spatial9 tools. Call get_capabilities first. "
                         "Always pass an idempotency_key. Poll every 45 s.",
            mcp_servers=[spatial9],
        )
        producer = Agent(
            name="Producer",
            instructions="Talk to the artist. Hand off any rendering or "
                         "remixing to the Spatial mixer.",
            handoffs=[mixer],
        )
        result = await Runner.run(producer, "Make my demo feel like a live concert.")
        print(result.final_output)

asyncio.run(main())
```


### Google ADK, and turning it into an A2A agent

Google's Agent Development Kit has both halves built in: McpToolset to use Spatial9, and to_a2a to publish the finished agent to other agents.

```python
import os
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
from google.adk.a2a.utils.agent_to_a2a import to_a2a

root_agent = Agent(
    model="gemini-2.5-flash",
    name="spatial_audio_agent",
    description="Turns stereo songs and videos into Eclipsa Audio with "
                "Spatial9, remixes in 3D and returns download links.",
    instruction="Call get_capabilities first. Use idempotency keys. "
                "Never publish without explicit approval of title, "
                "description and privacy.",
    tools=[McpToolset(
        connection_params=StreamableHTTPConnectionParams(
            url="https://spatial9.ai/mcp",
            headers={"Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}"},
        ),
        tool_filter=["get_capabilities", "create_upload_link",
                     "create_spatial_audio", "get_job_status",
                     "get_downloads", "get_mix", "remix_render"],
    )],
)

app = to_a2a(root_agent, port=8001)
# Run with: uvicorn spatial_agent:app --port 8001
```

The tool_filter matters here. Because this agent will take requests from other agents, it gets the create and remix tools but not publish_to_youtube or cancel_render.


### AutoGen and AG2: a mixer and a critic

AutoGen pioneered conversational multi-agent teams, and AG2 continues the original design as a community project. Microsoft now points new projects towards its Agent Framework, but the pattern below carries over. Two agents share the Spatial9 tools: a mixer proposes changes and a critic checks them with free previews until it is satisfied.

```python
import asyncio, os
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.tools.mcp import StreamableHttpServerParams, mcp_server_tools

async def main():
    params = StreamableHttpServerParams(
        url="https://spatial9.ai/mcp",
        headers={"Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}"},
    )
    tools = await mcp_server_tools(params)
    model = OpenAIChatCompletionClient(model="gpt-4o")

    mixer = AssistantAgent("mixer", model_client=model, tools=tools,
        system_message="Read get_mix, then propose remix_render changes "
                       "with preview: true. Never render without preview.")
    critic = AssistantAgent("critic", model_client=model,
        system_message="Check each preview against the brief: vocals front "
                       "and centre, drums wide, guitar moving in the chorus. "
                       "Reply APPROVED when it matches.")

    team = RoundRobinGroupChat([mixer, critic],
                               termination_condition=TextMentionTermination("APPROVED"),
                               max_turns=8)
    await Console(team.run_stream(task="Refine the mix of job 'JOB_ID'."))

asyncio.run(main())
```

remix_render with preview: true is a free dry run that shows exactly what would change, so a critic loop costs nothing until the final render.


## Data platform integrations: Databricks, Snowflake, Vertex AI and more

Data platforms are where a label, publisher or broadcaster keeps the catalog: track metadata, rights, release dates and the location of every master and stem. They are the natural place to decide which songs or videos become immersive, and a scheduled job inside the platform is the most reliable way to run a large conversion without anyone watching a chat window.

[Image: Inside the catalog. The data platform picks the tracks, Spatial9 turns them into immersive sound, and the results go back on the shelf.]

Every data platform integration uses the same pattern:

- Keep a render queue table with one row per track: an ID, a title, where the file lives, a priority and a status.
- A scheduled job wakes up every 10 to 15 minutes. It first checks the jobs already running with get_job_status and saves the files from get_downloads when they finish.
- It then starts new renders until three are running, using the track ID in the idempotency_key so a retried run never starts the same track twice.
- Spatial9 needs to reach the audio. Either hand it a short-lived signed link to the file as source_url, or have the job call create_upload_link and PUT the file, which keeps your storage completely private.

[Image: One loop for every data platform. The queue table is the memory, so a job can stop and restart at any time without losing work.]

| Platform | Where the files live | How Spatial9 gets the audio | What runs the loop | Where the key lives |
| Databricks | Unity Catalog volumes | create_upload_link, then PUT | Databricks Jobs | Databricks secret scope |
| Snowflake | External stage on S3, GCS or Azure | GET_PRESIGNED_URL as source_url | Snowflake Tasks | Snowflake secret with external access |
| Google Cloud | Cloud Storage, catalog in BigQuery | V4 signed URL as source_url | Vertex AI Agent Engine or Cloud Scheduler | Secret Manager |
| AWS | Amazon S3 | Presigned URL as source_url | Step Functions or EventBridge Scheduler | Secrets Manager |
| Microsoft Fabric | OneLake | create_upload_link, then PUT | Fabric data pipelines | Azure Key Vault |
| Dataiku | Managed folders | create_upload_link, then PUT | Scenarios | User or project secrets |

The server limits apply to every route: 3 renders at once and 20 per 24 hours per account. A 200-track backlog therefore takes about ten days, and the loop simply picks up where it stopped each time.


### Databricks

A Databricks notebook scheduled as a job reads the queue from a Delta table, uploads masters straight from a Unity Catalog volume and writes the results back. The key stays in a secret scope and never appears in the notebook.

```sql
CREATE TABLE media.immersive.render_queue (
  track_id STRING, title STRING, volume_path STRING, priority INT,
  status STRING, stage STRING, job_id STRING, files STRING,
  updated_at TIMESTAMP
);
```

```python
import json, requests

KEY = dbutils.secrets.get(scope="spatial9", key="api_key")
HEADERS = {"Authorization": f"Bearer {KEY}",
           "Accept": "application/json, text/event-stream"}
QUEUE = "media.immersive.render_queue"
DONE = ("pending", "completed", "failed", "cancelled")

def call(tool, args):
    body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
            "params": {"name": tool, "arguments": args}}
    r = requests.post("https://spatial9.ai/mcp", json=body, headers=HEADERS, timeout=60)
    r.raise_for_status()
    result = r.json()["result"]
    if result.get("isError"):
        raise RuntimeError(result["content"][0]["text"])
    return result.get("structuredContent", {})

def save(track_id, job, files=None):
    spark.sql(f"""UPDATE {QUEUE} SET job_id = :job, status = :status, stage = :stage,
                  files = :files, updated_at = current_timestamp() WHERE track_id = :id""",
              args={"job": job["job_id"], "status": job["status"], "stage": job.get("stage", ""),
                    "files": json.dumps(files or {}), "id": track_id})

for row in spark.sql(f"SELECT track_id, job_id FROM {QUEUE} "
                     f"WHERE job_id IS NOT NULL AND status NOT IN {DONE}").collect():
    job = call("get_job_status", {"job_id": row.job_id})
    files = call("get_downloads", {"job_id": row.job_id}) if job["status"] == "completed" else None
    save(row.track_id, job, files)

running = spark.sql(f"SELECT count(*) FROM {QUEUE} "
                    f"WHERE job_id IS NOT NULL AND status NOT IN {DONE}").first()[0]
pending = spark.sql(f"SELECT track_id, title, volume_path FROM {QUEUE} "
                    f"WHERE status = 'pending' ORDER BY priority LIMIT {max(0, 3 - running)}").collect()

for row in pending:
    try:
        up = call("create_upload_link", {"filename": row.volume_path.split("/")[-1],
                                         "media_type": "music"})
        with open(row.volume_path, "rb") as f:
            requests.put(up["direct_upload_url"], data=f, timeout=900).raise_for_status()
        job = call("create_spatial_audio", {"upload_id": up["upload_id"], "media_type": "music",
                                            "title": row.title,
                                            "idempotency_key": f"dbx-{row.track_id}"})
        save(row.track_id, job)
    except RuntimeError as e:
        print(f"Stopping for now: {e}")
        break
```

Schedule the notebook as a Databricks job every 15 minutes. When the daily limit is reached, Spatial9 returns a plain explanation, the loop stops and the next run continues. Teams building agents with Databricks' own agent tooling can also connect the same spatial9.ai/mcp address wherever their workspace supports external MCP servers.


### Snowflake

Snowflake runs the loop as a Python stored procedure. Three objects allow it to call Spatial9 safely: a network rule that permits only spatial9.ai, a secret that holds the key, and an external access integration that ties them together. Because Snowflake can create a presigned link to a file on an external stage, Spatial9 downloads the master directly and no file passes through the warehouse.

```sql
CREATE OR REPLACE NETWORK RULE spatial9_egress
  MODE = EGRESS TYPE = HOST_PORT VALUE_LIST = ('spatial9.ai');

CREATE OR REPLACE SECRET spatial9_key
  TYPE = GENERIC_STRING SECRET_STRING = 's9_your_personal_key';

CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION spatial9_access
  ALLOWED_NETWORK_RULES = (spatial9_egress)
  ALLOWED_AUTHENTICATION_SECRETS = (spatial9_key)
  ENABLED = TRUE;

CREATE OR REPLACE PROCEDURE spatial9_sync()
  RETURNS STRING
  LANGUAGE PYTHON
  RUNTIME_VERSION = '3.11'
  PACKAGES = ('snowflake-snowpark-python', 'requests')
  HANDLER = 'run'
  EXTERNAL_ACCESS_INTEGRATIONS = (spatial9_access)
  SECRETS = ('key' = spatial9_key)
AS
$$
import json, requests, _snowflake

HEADERS = {"Authorization": "Bearer " + _snowflake.get_generic_secret_string("key"),
           "Accept": "application/json, text/event-stream"}
QUEUE = "MEDIA.IMMERSIVE.RENDER_QUEUE"
ACTIVE = "job_id IS NOT NULL AND status NOT IN ('completed', 'failed', 'cancelled')"

def call(tool, args):
    r = requests.post("https://spatial9.ai/mcp", headers=HEADERS, timeout=60, json={
        "jsonrpc": "2.0", "id": 1, "method": "tools/call",
        "params": {"name": tool, "arguments": args}})
    r.raise_for_status()
    result = r.json()["result"]
    if result.get("isError"):
        raise RuntimeError(result["content"][0]["text"])
    return result.get("structuredContent", {})

def run(session):
    checked = session.sql(f"SELECT track_id, job_id FROM {QUEUE} WHERE {ACTIVE}").collect()
    for row in checked:
        job = call("get_job_status", {"job_id": row["JOB_ID"]})
        files = call("get_downloads", {"job_id": row["JOB_ID"]}) if job["status"] == "completed" else {}
        session.sql(f"UPDATE {QUEUE} SET status = ?, stage = ?, files = PARSE_JSON(?) WHERE track_id = ?",
                    params=[job["status"], job.get("stage", ""), json.dumps(files), row["TRACK_ID"]]).collect()

    running = session.sql(f"SELECT COUNT(*) AS n FROM {QUEUE} WHERE {ACTIVE}").collect()[0]["N"]
    pending = session.sql(
        f"SELECT track_id, title, GET_PRESIGNED_URL(@media.immersive.masters, file_path, 7200) AS url "
        f"FROM {QUEUE} WHERE status = 'pending' ORDER BY priority LIMIT {max(0, 3 - running)}").collect()
    for row in pending:
        job = call("create_spatial_audio", {"source_url": row["URL"], "media_type": "music",
                                            "title": row["TITLE"],
                                            "idempotency_key": f"snow-{row['TRACK_ID']}"})
        session.sql(f"UPDATE {QUEUE} SET job_id = ?, status = ? WHERE track_id = ?",
                    params=[job["job_id"], job["status"], row["TRACK_ID"]]).collect()
    return f"{len(checked)} checked, {len(pending)} started"
$$;

CREATE OR REPLACE TASK spatial9_every_10_minutes
  WAREHOUSE = media_wh
  SCHEDULE = '10 MINUTE'
AS
  CALL spatial9_sync();

ALTER TASK spatial9_every_10_minutes RESUME;
```

The network rule is the important guardrail: the procedure can reach Spatial9 and nothing else.


### Google Cloud: Vertex AI Agent Engine and BigQuery

On Google Cloud, the Google ADK agent from the framework section above can run as a managed service on Vertex AI Agent Engine, with the catalog in BigQuery and the masters in Cloud Storage. Give the agent one extra tool that turns a gs:// path into a two-hour signed link, and it can pass that link to Spatial9 as source_url.

```python
from datetime import timedelta
from google.cloud import storage

gcs = storage.Client()

def signed_link(gcs_uri: str) -> str:
    """Return a two-hour download link for a file in Cloud Storage."""
    blob = storage.Blob.from_string(gcs_uri, client=gcs)
    return blob.generate_signed_url(version="v4", expiration=timedelta(hours=2), method="GET")
```

Add signed_link to the agent's tools list next to the Spatial9 McpToolset, then deploy it.

```python
import vertexai
from vertexai import agent_engines
from vertexai.preview import reasoning_engines
from spatial_agent import root_agent

vertexai.init(project="my-label-project", location="us-central1",
              staging_bucket="gs://my-label-agent-staging")

remote = agent_engines.create(
    reasoning_engines.AdkApp(agent=root_agent, enable_tracing=True),
    requirements=["google-cloud-aiplatform[adk,agent_engines]", "google-cloud-storage"],
    display_name="spatial-audio-agent",
)

for event in remote.stream_query(
    user_id="label-ops",
    message="Render the three highest-priority pending tracks from "
            "media.render_queue in BigQuery and report their job IDs.",
):
    print(event)
```

Give the agent a BigQuery tool as well, such as the BigQuery toolset that ships with ADK, so it can read the queue and write results back. Store the Spatial9 key in Secret Manager and supply it to the deployed agent as an environment variable rather than in code. Signing links requires a service account that is allowed to sign, so grant that role only to the agent's own account. For teams on Gemini Enterprise, the same agent can be published to employees, or exposed over A2A as described in the next section.


### AWS: S3, Step Functions and Bedrock AgentCore

On AWS, the files usually sit in S3. A presigned link gives Spatial9 two hours to download a master without making the bucket public.

```python
import boto3

s3 = boto3.client("s3")
url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": "label-masters", "Key": "2026/night-drive.wav"},
    ExpiresIn=7200,
)
job = call("create_spatial_audio", {"source_url": url, "media_type": "music",
                                    "title": "Night Drive",
                                    "idempotency_key": "aws-night-drive"})
```

Step Functions suits the wait: a Lambda starts the render, a Wait state pauses for 60 seconds, another Lambda calls get_job_status, and a Choice state loops until the status is completed or failed. A final Lambda copies the get_downloads files into S3 and updates DynamoDB or Athena. For agent workloads, Amazon Bedrock AgentCore can host the agent itself and manage its tool connections; register spatial9.ai/mcp as an MCP tool where your AgentCore setup supports remote MCP servers.


### Microsoft Fabric, Palantir AIP, Dataiku and Glean

Microsoft Fabric notebooks run the same Python as the Databricks example. Read the key from Azure Key Vault with notebookutils.credentials.getSecret, read files from OneLake, and schedule the notebook in a Fabric data pipeline.

Palantir AIP models the catalog as Ontology objects. A "Track" object with an action that starts a render, backed by an outbound call to Spatial9 through Palantir's data connection and webhook features, lets AIP agents and human operators trigger conversions with the same permissions that already govern the catalog. Status and links are written back as object properties.

Dataiku fits the loop into a scenario: a Python step runs the polling helper on a dataset of pending tracks, reads masters from a managed folder and uploads them with create_upload_link. The key belongs in Dataiku's secrets, not in the project code.

Glean is about finding things rather than rendering them. Index the queue table or the delivered stage player links so anyone can ask "where is the immersive version of Night Drive?" and get the answer with its files.


## The Spatial9 A2A agent

Spatial9 runs its own A2A agent. Any A2A client, such as an Agentforce, Copilot Studio, watsonx Orchestrate or ADK agent, can discover it and hand it work without knowing anything about MCP.

[Image: Discovery in A2A starts with an Agent Card: a short public description of who the agent is and what it can do.]

| Detail | Value |
| Endpoint | https://spatial9.ai/a2a |
| Agent Card | https://spatial9.ai/.well-known/agent-card.json |
| Transport | JSON-RPC 2.0 over HTTP POST, A2A protocol version 0.3.0 |
| Methods | message/send, message/stream, tasks/get, tasks/resubscribe, tasks/cancel, and tasks/pushNotificationConfig/set, get, list and delete |
| Sign-in | A personal s9_ key as a Bearer token or X-API-Key, or OAuth 2.1 with PKCE (scope spatial_audio) |
| Updates | Live stream with message/stream and tasks/resubscribe, https webhooks, or polling tasks/get |
| Limits | The same as MCP: 3 renders at once, 20 per 24 hours, failed renders refunded |


### Skills on the Agent Card

| Skill id | What it does | Typical time |
| spatial_audio | Song or video to Eclipsa Audio with a binaural headphone version (runs create_spatial_audio) | 10 to 20 minutes per song, a few minutes per short video |
| split_stems | Separates a song into instrument stems as WAV files | 4 to 8 minutes |
| convert_dolby | AC-3, E-AC-3, TrueHD or ADM BWF to Eclipsa Audio | Depends on length |
| preview_chorus | Free 30-second binaural preview of the chorus | Under two minutes |
| upload_link | Returns an upload_id and a direct PUT URL, up to 2 GB, valid 2 hours | Immediate |

Pick a skill with a data part such as {"skill": "split_stems"}. Without one, the agent reads the text: "stems" picks split_stems, "Dolby" or "ADM" picks convert_dolby, "preview" or "chorus" picks preview_chorus, and anything else is a full spatial_audio render. Dolby Atmos is an input only: for AC-3, E-AC-3 and TrueHD the speaker channels, up to 7.1, are decoded and the Atmos height and object layer is not kept.


### Sending it a task

The client sends a message and gets a task back straight away. The file can be a link in the text, a file part with a uri, or a data part with source_url or upload_id.

```bash
curl -s https://spatial9.ai/a2a \
  -H "Authorization: Bearer $SPATIAL9_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "req-1",
    "method": "message/send",
    "params": {
      "message": {
        "kind": "message",
        "role": "user",
        "messageId": "night-drive-001",
        "parts": [
          { "kind": "text", "text": "Make Night Drive immersive, cinema style." },
          { "kind": "data", "data": {
              "skill": "spatial_audio",
              "source_url": "https://example.com/night-drive.wav",
              "title": "Night Drive",
              "style": "cinema" } }
        ]
      }
    }
  }'
```

The messageId doubles as the idempotency key. Sending the same messageId again returns the same task instead of starting, and charging for, a second render.


### Checking, collecting and cancelling

```python
import os, time, uuid, httpx

A2A = "https://spatial9.ai/a2a"
HEADERS = {"Authorization": f"Bearer {os.environ['SPATIAL9_KEY']}"}

def rpc(method, params):
    r = httpx.post(A2A, headers=HEADERS, timeout=60,
                   json={"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method, "params": params})
    r.raise_for_status()
    body = r.json()
    if "error" in body:
        raise RuntimeError(body["error"]["message"])
    return body["result"]

task = rpc("message/send", {"message": {
    "kind": "message", "role": "user", "messageId": "stems-042",
    "parts": [{"kind": "data", "data": {"skill": "split_stems",
                                        "source_url": "https://example.com/song.wav"}}]}})

while task["status"]["state"] in ("submitted", "working"):
    time.sleep(45)
    task = rpc("tasks/get", {"id": task["id"], "historyLength": 0})

if task["status"]["state"] == "completed":
    for artifact in task.get("artifacts", []):
        for part in artifact["parts"]:
            if part["kind"] == "file":
                print(artifact["name"], part["file"]["uri"])
else:
    print(task["status"]["state"], task["status"]["message"]["parts"][0]["text"])

# To stop a render that is still running:
# rpc("tasks/cancel", {"id": task["id"]})
```

A finished task carries one file artifact per delivered file, with its URL, MIME type and filename, plus a summary data artifact with the job_id and, for music, the before and after comparison link and the stage player link.


### How render stages map to task states

[Image: One small lookup table turns a Spatial9 render into a well-behaved A2A task.]

| Spatial9 | A2A task state | What the agent returns |
| Task accepted | submitted | The task id and context id |
| Separating, placing and rendering | working | The current stage and progress percentage |
| No file or filename in the message | input-required | What to send next; reply with the same taskId |
| completed | completed | The delivered files as artifacts |
| failed | failed | A plain explanation; the project is refunded automatically |
| cancelled | canceled | Confirmation after tasks/cancel |

A chorus preview is free and finishes in about two minutes, so tasks/cancel answers it with TaskNotCancelableError.


### Live updates and webhooks

There are three ways for a client agent to follow a task:

| Way | How | Best for |
| Live stream | Send message/stream with Accept: text/event-stream. The agent sends the task, then a status-update event for each stage and artifact-update events with the files. | Chat interfaces and interactive agents |
| Webhook | Add configuration.pushNotificationConfig to message/send, or call tasks/pushNotificationConfig/set later. Spatial9 posts the full task to your address each time its state changes. | Back-end workflows and orchestrators |
| Polling | Call tasks/get every 30 to 60 seconds. | Simple scripts |

Streams through spatial9.ai/a2a close after about 20 seconds because of the web host's request limit. Renders take minutes, so a client calls tasks/resubscribe with the task ID to pick up the stream again, in a loop, until it receives an event marked final.

```bash
curl -N https://spatial9.ai/a2a \
  -H "Authorization: Bearer $SPATIAL9_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"jsonrpc":"2.0","id":"1","method":"message/stream","params":{"message":{"kind":"message","role":"user","messageId":"song-001","parts":[{"kind":"data","data":{"skill":"spatial_audio","source_url":"https://example.com/song.wav","title":"My Song"}}]}}}'
```

A webhook is registered the same way, with an address and a token you choose:

```json
"configuration": {
  "pushNotificationConfig": {
    "url": "https://your-server.example/a2a-updates",
    "token": "a-secret-you-choose"
  }
}
```

Webhook rules:

- The address must use https on a public host and port 443. Private and internal addresses are refused.
- Each delivery carries your token in the X-A2A-Notification-Token header so your server can confirm the call came from Spatial9. Add authentication with a scheme and credentials to also receive an Authorization header.
- A task can have up to five webhooks. Deliveries are checked once a minute, so a notification can arrive up to a minute after the change.
- A webhook stops after five failed deliveries in a row or after two days. tasks/pushNotificationConfig/get and list never return the credentials you sent.


### Building your own A2A specialist

The Spatial9 agent does not remix finished renders or publish to YouTube; those stay on the MCP server, where publishing needs a confirmed title, description and privacy. When your A2A specialist must do those, build it with the ADK code above and to_a2a, which generates a card like this one.

```json
{
  "name": "Spatial Audio Agent",
  "description": "Turns stereo songs and videos into Eclipsa Audio with a headphone version using Spatial9, and remixes instruments in 3D.",
  "url": "https://agents.example.com/spatial-audio",
  "version": "1.0.0",
  "protocolVersion": "0.3.0",
  "capabilities": { "streaming": true, "pushNotifications": false },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    {
      "id": "render-eclipsa",
      "name": "Render Eclipsa Audio",
      "description": "Separate a song, place each instrument in 3D and deliver IAMF, MP4 and binaural files.",
      "tags": ["spatial audio", "eclipsa", "iamf", "immersive"],
      "examples": ["Make this track feel like a live concert: https://..."]
    },
    {
      "id": "remix-3d",
      "name": "Remix in 3D",
      "description": "Move, rebalance or animate instruments in a finished render.",
      "tags": ["remix", "mix", "motion"],
      "examples": ["Circle the guitar around the listener in the chorus"]
    }
  ],
  "securitySchemes": { "bearer": { "type": "http", "scheme": "bearer" } },
  "security": [{ "bearer": [] }]
}
```

In your own wrapper, map a publish that still needs a yes to input-required until a person confirms. Treat messages from other agents as untrusted input. The wrapper should only ever call the tools in its filter, should keep its own Spatial9 key out of every reply, and should never let a peer's message override the publish rule.


## Six integration blueprints

These combinations come up again and again in conversations with labels, creators and post houses.

[Image: Six real workflows. Each card names the platforms that usually drive it and the Spatial9 tools it calls.]


### Label catalog swarm

A Databricks or Snowflake query selects the catalog tracks cleared for immersive release. A CrewAI crew works through the list: one agent fetches stems or masters, one renders with create_spatial_audio and a per-track idempotency_key, one writes the delivered file links back to the catalog table. The crew respects the server limits of 3 renders at once and 20 per day, so a 200-track backlog is a ten-day job that runs itself.


### Release-day concierge

An artist-services team runs Agentforce or Copilot Studio as the front door. When an artist asks for an immersive version, that agent sends an A2A task to the Spatial9 A2A agent at spatial9.ai/a2a: a preview_chorus task first, then a spatial_audio render. For the YouTube step, a custom ADK specialist prepares a publish_to_youtube call and keeps its task in input-required until the artist confirms the title, description and privacy.


### Creator channel automation

An n8n workflow or Zapier zap watches a cloud folder. When a new video lands, it calls create_upload_link, uploads the file, starts create_spatial_audio with media_type set to video and posts the YouTube-ready MP4 link to the team's chat when the job completes. No language model is needed for the happy path; add one only to write the description.


### Post-house Atmos bridge

A ServiceNow or Jira ticket moves to "deliver open format". A UiPath robot or a small agent calls convert_dolby_to_eclipsa with the ADM BWF master and attaches the Eclipsa Audio files to the ticket. Dolby Atmos is an input here, never an output: Spatial9 renders a new Eclipsa Audio mix from the ADM objects, and AC-3, E-AC-3 and TrueHD sources are decoded up to 7.1. Our explainers on ADM BWF files and Eclipsa Audio versus Dolby Atmos cover the formats in depth.


### Classroom tutor

A school or course platform connects Gemini, Claude or ChatGPT to the free address spatial9.ai/mcp/free. Students ask questions, and the tutor uses learn_spatial_audio, list_lessons, get_lesson and try_a_sample to explain Ambisonics or height channels with a sound they can hear, without anyone creating an account.


### Mix critic loop

An AutoGen or LangGraph pair, as in the code above, iterates on a finished render: get_mix to read positions, remix_render with preview: true to propose changes, a critic to compare against the brief, and one real render at the end. Remixes typically take 3 to 6 minutes.

[Image: A happy crew. Every specialist talks to the same Spatial9 server; only the tools each one is allowed to use differ.]


## Guardrails in layers

Agents that render and publish need more than one safety net. The good news is that each layer is owned by a different tool, so no single one has to be perfect.

[Image: Trust in layers. Each one stops a different kind of mistake.]

- The Spatial9 server enforces what it can: one OAuth scope (spatial_audio), idempotency keys, 3 concurrent renders and 20 per day, 5 YouTube publishes per day, revocable personal keys and automatic refunds for failed renders. Its tool descriptions also tell every agent to confirm the title, description and privacy before publishing.
- Your agent policy decides which tools each agent may call. Give a classroom tutor the learning tools only, and keep publish_to_youtube away from any agent that takes requests from outside your team.
- Connectors and credentials. Composio and Pipedream Connect can hold a separate Spatial9 key or OAuth grant per end user, so a multi-tenant product never shares one account.
- Governance and observability. Zenity and Credo AI apply security and AI policy across agents; Agentic Fabriq and CloudEagle.ai keep an inventory of what has access to what; Galileo evaluates and traces agent runs so you can see why an agent chose a style or a privacy setting.
- A human in the loop approves anything public and anything that spends a project. Previews are free, so the human can listen before saying yes.

[Image: The last layer is a person. Agents prepare everything, and a human taps approve before anything goes public.]


## Which route should you choose?

| If you are | Start with | Why |
| A developer building one agent | OpenAI Agents SDK, LangGraph or ADK with MCP | Full tool descriptions, least code |
| A team building a multi-agent product | CrewAI or AutoGen and AG2 with filtered tools | Clear roles, one tool set per role |
| An enterprise with an agent studio | Register the MCP server, or send A2A tasks to spatial9.ai/a2a | Keeps audio work behind a governed boundary |
| An automation team | n8n, Zapier or UiPath with the raw JSON-RPC call | Event-driven, no model needed for the happy path |
| A data team with a catalog | Databricks Jobs, Snowflake Tasks or Vertex AI Agent Engine with a queue table | Batches, retries, idempotency and an audit trail in the table |
| A product embedding audio for its own users | The Spatial9 REST API | Direct uploads and status checks, no agent required |

For deeper background on what the renders do, our summary of 124 papers on AI spatial audio explains the separation, placement and rendering research behind each step.


## Frequently asked questions


### Does Spatial9 support the A2A protocol?

Yes. The Spatial9 A2A agent answers at https://spatial9.ai/a2a and publishes its Agent Card at https://spatial9.ai/.well-known/agent-card.json. It supports message/send, message/stream, tasks/get, tasks/resubscribe, tasks/cancel and push notification webhooks with five skills: immersive renders, stem splits, Dolby and ADM conversion, chorus previews and upload links. Clients can follow a render live, receive webhook calls, or poll tasks/get. Remixing and YouTube publishing stay on the MCP server.


### How do I sign in to the Spatial9 A2A agent?

Send a personal s9_ key from spatial9.ai/connect as Authorization: Bearer or X-API-Key, or use OAuth 2.1 with PKCE and the spatial_audio scope; both are listed in the Agent Card. Renders use projects from the signed-in Spatial9 plan, and the free no-sign-in address spatial9.ai/mcp/free is MCP only.


### What is the difference between MCP and A2A?

MCP connects an agent to tools, such as the 37 Spatial9 tools. A2A connects one agent to another agent so it can delegate a task and track its progress. Most real systems use both: A2A between agents, MCP from each agent to its tools.


### Which agent frameworks work with Spatial9?

Any framework with an MCP client that supports remote HTTP servers, including LangGraph, CrewAI, AutoGen, AG2, the OpenAI Agents SDK, Google ADK and Goose. Platforms without an MCP client can send a single HTTP POST with a JSON-RPC body.


### How do agents handle renders that take 10 to 20 minutes?

They start the job once with an idempotency_key, poll get_job_status every 30 to 60 seconds and then call get_downloads. Spatial9 also emails the links when a job finishes, so nobody has to wait at a screen.


### Can an agent publish to YouTube without a person approving it?

It should not. The Spatial9 tools tell every agent to confirm the title, description and privacy with the user before calling publish_to_youtube, and each account is limited to 5 publishes per day. Your agent policy should add a human approval step as well.


### Is there a free way to test an integration?

Yes. The address https://spatial9.ai/mcp/free needs no sign-in and offers chorus previews, a video conversion and an ADM to Eclipsa conversion. remix_render with preview: true is also free for signed-in users.


### Can I run Spatial9 from Databricks, Snowflake or BigQuery?

Yes. A scheduled job keeps a render queue table, starts up to three renders at a time with an idempotency_key per track, polls get_job_status and writes the get_downloads links back to the table. Snowflake and Google Cloud can hand Spatial9 a short-lived signed link to each master; Databricks and Microsoft Fabric can upload the file with create_upload_link.


### What is ACP and should I use it?

The Agent Communication Protocol came from IBM's BeeAI project and has since joined the A2A effort under the Linux Foundation. For new agent-to-agent work, target A2A.


## Sources

- Agent2Agent (A2A) Protocol specification and documentation
- A2A project repository
- Model Context Protocol specification
- Google Agent Development Kit documentation, including MCP tools and A2A
- OpenAI Agents SDK, MCP guide
- OpenAI Swarm (experimental, superseded by the Agents SDK)
- LangGraph documentation
- LangChain MCP adapters
- CrewAI documentation, MCP integration
- Microsoft AutoGen documentation
- AG2
- n8n documentation
- Zapier MCP
- Salesforce Agentforce
- Microsoft Copilot Studio documentation
- Amazon Bedrock AgentCore
- Databricks secret management
- Databricks Jobs
- Snowflake external network access
- Snowflake GET_PRESIGNED_URL
- Snowflake tasks
- Vertex AI Agent Engine
- Cloud Storage signed URLs
- Amazon S3 presigned URLs
- Microsoft Fabric notebook utilities
- Galileo, AI agent evaluation and observability
- Composio
- Pipedream Connect
- Spatial9 connection guide
- Spatial9 REST API
- Agentic AI Meets Spatial Audio, Spatial9 Engineering: /blog/agentic-ai-spatial-audio-mcp

Product and company names in this article are trademarks of their respective owners and are used only to describe integrations. Third-party features change often; check each vendor's documentation for current MCP and A2A support.

