Skip to main content
POST
/
v2
/
agents
/
create
Create Agent
curl --request POST \
  --url https://api.velt.dev/v2/agents/create \
  --header 'Content-Type: application/json' \
  --header 'x-velt-api-key: <x-velt-api-key>' \
  --header 'x-velt-auth-token: <x-velt-auth-token>' \
  --data '
{
  "data": {
    "name": "<string>",
    "description": "<string>",
    "rawInstructions": "<string>",
    "instructions": "<string>",
    "enabled": true,
    "metadata": {},
    "contextGathering": {},
    "execution": {},
    "response": {},
    "postProcess": {},
    "input": {},
    "scope": {},
    "setup": {}
  }
}
'
import requests

url = "https://api.velt.dev/v2/agents/create"

payload = { "data": {
"name": "<string>",
"description": "<string>",
"rawInstructions": "<string>",
"instructions": "<string>",
"enabled": True,
"metadata": {},
"contextGathering": {},
"execution": {},
"response": {},
"postProcess": {},
"input": {},
"scope": {},
"setup": {}
} }
headers = {
"x-velt-api-key": "<x-velt-api-key>",
"x-velt-auth-token": "<x-velt-auth-token>",
"Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.text)
const options = {
method: 'POST',
headers: {
'x-velt-api-key': '<x-velt-api-key>',
'x-velt-auth-token': '<x-velt-auth-token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
data: {
name: '<string>',
description: '<string>',
rawInstructions: '<string>',
instructions: '<string>',
enabled: true,
metadata: {},
contextGathering: {},
execution: {},
response: {},
postProcess: {},
input: {},
scope: {},
setup: {}
}
})
};

fetch('https://api.velt.dev/v2/agents/create', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.velt.dev/v2/agents/create",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'data' => [
'name' => '<string>',
'description' => '<string>',
'rawInstructions' => '<string>',
'instructions' => '<string>',
'enabled' => true,
'metadata' => [

],
'contextGathering' => [

],
'execution' => [

],
'response' => [

],
'postProcess' => [

],
'input' => [

],
'scope' => [

],
'setup' => [

]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-velt-api-key: <x-velt-api-key>",
"x-velt-auth-token: <x-velt-auth-token>"
],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"strings"
"net/http"
"io"
)

func main() {

url := "https://api.velt.dev/v2/agents/create"

payload := strings.NewReader("{\n \"data\": {\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"rawInstructions\": \"<string>\",\n \"instructions\": \"<string>\",\n \"enabled\": true,\n \"metadata\": {},\n \"contextGathering\": {},\n \"execution\": {},\n \"response\": {},\n \"postProcess\": {},\n \"input\": {},\n \"scope\": {},\n \"setup\": {}\n }\n}")

req, _ := http.NewRequest("POST", url, payload)

req.Header.Add("x-velt-api-key", "<x-velt-api-key>")
req.Header.Add("x-velt-auth-token", "<x-velt-auth-token>")
req.Header.Add("Content-Type", "application/json")

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.velt.dev/v2/agents/create")
.header("x-velt-api-key", "<x-velt-api-key>")
.header("x-velt-auth-token", "<x-velt-auth-token>")
.header("Content-Type", "application/json")
.body("{\n \"data\": {\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"rawInstructions\": \"<string>\",\n \"instructions\": \"<string>\",\n \"enabled\": true,\n \"metadata\": {},\n \"contextGathering\": {},\n \"execution\": {},\n \"response\": {},\n \"postProcess\": {},\n \"input\": {},\n \"scope\": {},\n \"setup\": {}\n }\n}")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.velt.dev/v2/agents/create")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["x-velt-api-key"] = '<x-velt-api-key>'
request["x-velt-auth-token"] = '<x-velt-auth-token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"data\": {\n \"name\": \"<string>\",\n \"description\": \"<string>\",\n \"rawInstructions\": \"<string>\",\n \"instructions\": \"<string>\",\n \"enabled\": true,\n \"metadata\": {},\n \"contextGathering\": {},\n \"execution\": {},\n \"response\": {},\n \"postProcess\": {},\n \"input\": {},\n \"scope\": {},\n \"setup\": {}\n }\n}"

response = http.request(request)
puts response.read_body
{
  "result": {
    "status": "success",
    "message": "Agent created successfully",
    "data": {
      "agentId": "abc123def456"
    }
  }
}
Use this API to create a new custom agent configuration. The engine stores the agent in Firestore and writes version 1 to the agent’s versions subcollection. The schema uses .passthrough(), so any additional behavioral fields are forwarded to the service layer.

Endpoint

POST https://api.velt.dev/v2/agents/create

Headers

x-velt-api-key
string
required
Your API key.
x-velt-auth-token
string
required

Body

Params

data
object
required

Server-managed fields

The following fields are owned by the server and are rejected with a 400 INVALID_ARGUMENT if present on the create or update-version payload:
FieldWhy it is server-managed
managedByAlways derived server-side — "customer" for every API-created agent.
metadata.typeMarks built-in / system agents.
metadata.categoryMarks built-in / system agents.
metadata.internaltrue hides an agent from all list responses — allowing a client to set it would forge a hidden agent.
metadata.apiKeyThe tenant key; merged in server-side.
id, version, createdAt, and updatedAt are likewise server-generated. Sending a reserved key returns one error per offending key, e.g. metadata.internal is a server-managed field and cannot be set via the API.

Context gathering strategies

StrategyWhat it returns
web-page-textVisible text extracted via Puppeteer
web-page-screenshotFull-page screenshot as image (multimodal input)
web-page-htmlCleaned HTML content
web-page-cssCleaned CSS content from stylesheets
web-page-linksAll hyperlinks, classified internal/external
web-page-accessibilityAccessibility issues (Pa11y / HTML_CodeSniffer)
computed-stylesBrowser-resolved computed CSS for specific selectors
robots-txtFetched robots.txt
sitemap-dataDiscovered and parsed XML sitemaps
lighthouseGoogle Lighthouse audit (performance / a11y / best-practices / SEO)
rest-apiCustomer-configured REST endpoints fetched at runtime (see below)
noneNo context gathering (for service-only or text-only agents)

Context gathering strategy options

Per-strategy overrides are supplied under contextGathering.strategyOptions["<strategy>"]. All are optional unless noted.
StrategyOptionTypeDescription
web-page-textdeviceTypestring"mobile" / "desktop" (default "desktop").
excludeIframesbooleanExclude iframe content. Default false.
web-page-screenshotscrollBeforeCapturebooleanScroll the full page before capture.
maxPageHeightPxnumberCap the captured page height.
waitUntilstringPuppeteer wait condition.
deviceTypestring"mobile" / "desktop".
web-page-htmlincludeHeadbooleanInclude <head>. Default true.
includeBodybooleanInclude <body>. Default true.
excludeIframesbooleanDefault false.
waitUntilstringPuppeteer wait condition.
web-page-cssincludeInlineStylesbooleanInclude inline <style> content.
computed-stylesselectorsstring[]Required — CSS selectors to inspect.
propertiesstring[]Override the curated default property list.
sitemap-datamaxEntriesnumberMax sitemap entries. Default 500.
lighthousecategoriesstring[]Lighthouse categories to run.
formFactorstring"mobile" / "desktop".
rest-api(see below)objectRequired when rest-api is selected — rest-api strategy options.

rest-api strategy options

Required when strategies includes "rest-api". Each configured endpoint is fetched at execution time and the resolved URL + description + JSON-stringified response is injected into the prompt via the {{restApiData}} template variable.
FieldTypeRequiredDescription
endpointsRestApiEndpoint[]yes1–10 endpoints to fetch.
maxResponseBytesnumbernoPer-endpoint response-body cap. Default 1000000, hard-capped 5000000.
RestApiEndpoint fields:
FieldTypeRequiredDescription
descriptionstringyesWhat this endpoint is about; surfaced in the prompt alongside the URL + response.
urlstringyesEndpoint URL. Supports {{variable}} templating. SSRF-validated at fetch time (no internal/loopback/link-local hosts).
method"GET"|"POST"|"PUT"|"PATCH"|"DELETE"noDefault "GET".
idstringnoStable endpoint identifier.
namestringnoHuman label.
headersobjectnoNon-secret headers (templated).
queryobjectnoQuery params (templated).
bodyobject | stringnoRequest body for write methods (templated).
authobjectnoDiscriminated union (none|bearer|basic|header). Secret fields are encrypted at rest and redacted on read.
cacheTtlSecondsnumbernoIn-process per-instance cache TTL. Default 0 (always fresh). Max 86400.
timeoutMsnumbernoPer-request timeout. Default 10000, hard-capped 30000.
responsePathstringnoDot-path to extract a sub-field of the JSON response.
auth shapes:
{ "type": "none" }
{ "type": "bearer", "token": "<secret>" }
{ "type": "basic", "username": "user", "password": "<secret>" }
{ "type": "header", "headers": { "X-Api-Key": "<secret>" } }
Auth secret handling (redaction-on-read): token, password, and header values are encrypted before the Firestore write and are never returned to clients. On read (Get / List Agents), encrypted secret fields are replaced with "__redacted__". To rotate a secret, send the new plaintext value on update; to keep an existing secret, omit the field.

MCP servers (mcp-tools strategy)

When execution.executionStrategy is "mcp-tools", the agent runs a model-driven tool loop: at execution time the engine connects to each remote MCP (Model Context Protocol) server declared in execution.mcpServers, lists its tools, exposes them to the model, and runs a multi-turn call→tool→call loop until the model emits a final structured response. This is how the built-in docs code-verification agent checks snippets live against the Velt docs MCP instead of a pre-ingested corpus.
The "mcp-tools" strategy is provider-agnostic (works on both Claude and Gemini) and requires a non-empty top-level instructions field and at least one entry in execution.mcpServers. Omitting either fails validation.
execution.mcpServers is an array of 1–5 server objects (MCP_MAX_SERVERS = 5). Each server object is strictly validated (unknown keys rejected):
FieldTypeRequiredDescription
idstringyesMin 1 char. Stable identifier for the server (used in logs + findings).
urlstringyesRemote MCP server URL. Must start with http(s):// or contain a {{template}} that resolves to one. SSRF-validated at connect time (no internal/loopback/link-local hosts; redirects re-validated, max 3 hops).
namestringnoHuman-readable label.
descriptionstringnoWhat this server provides (e.g. "Velt docs MCP").
transportstringnoTransport type. Only "http" is supported (Streamable HTTP with SSE fallback). Defaults to "http".
authobjectnoAuth config. Discriminated union (none | bearer | basic | header) — same shape as the rest-api strategy. Secret fields are encrypted at rest and redacted on read.
allowedToolsstring[]noAllowlist of tool names. When set, only these tools are exposed/callable. Up to 64 tools per server are exposed (MCP_MAX_TOOLS).
timeoutMsnumbernoPer-request timeout (ms) for connect/listTools/callTool. Default 30000, hard-capped 120000.
auth shapes (identical to the rest-api strategy):
{ "type": "none" }
{ "type": "bearer", "token": "<secret>" }
{ "type": "basic", "username": "user", "password": "<secret>" }
{ "type": "header", "headers": { "X-Api-Key": "<secret>" } }
Auth secret handling (redaction-on-read): MCP token, password, and header values are encrypted before the Firestore write (using an MCP-specific encryption salt) and are never returned to clients. On read (Get / List Agents, List Versions), encrypted secret fields are replaced with "__redacted__". To rotate a secret, send the new plaintext value on a version update; to keep an existing secret, omit the field.

AI config (provider / model)

Both execution.aiConfig and contextGathering.aiConfig accept the same strictly-validated object, letting an agent override the AI provider/model used for that phase. Unknown keys are rejected.
FieldTypeDescription
providerstringLLM provider: "gemini", "claude", or "openai". Defaults to the platform default when omitted.
modelstringModel name for the chosen provider (e.g. "gemini-3-flash-preview", "claude-sonnet-4-6").
responseMimeTypestringRequested response MIME type (e.g. "application/json").
maxToolTurnsnumberTool-loop turn budget for the mcp-tools strategy. Positive integer, hard-capped at runtime.

Execution strategy options

execution.strategyOptions["<strategy>"] carries per-strategy runtime overrides. The stagehand-agent strategy accepts:
OptionTypeDefaultDescription
modestring"hybrid""dom" / "hybrid" / "cua".
maxStepsnumber25Max autonomous steps.
modelstringinheritedModel override for the agent loop.
systemPromptstringSystem prompt for the agent.
useStructuredOutputbooleantrueReturn structured output.
waitUntilstring"networkidle2""domcontentloaded" / "load" / "networkidle2".
navigationTimeoutnumber30000Navigation timeout in ms.
When using stagehand-agent, set contextGathering.strategies: ["none"] — the agent handles navigation, interaction, and extraction itself.

Knowledge (Memory-RAG)

execution.knowledge pulls workspace knowledge from Velt Memory into the prompt. Strictly validated — only these four fields are accepted:
FieldTypeRangeDescription
useMemorybooleanWhen true, retrieves knowledge/patterns/activities and injects them as {{memoryContext}}.
maxChunksnumber1–20Max knowledge chunks to retrieve.
maxPatternsnumber1–20Max learned patterns to retrieve.
maxActivitiesnumber1–20Max recent activities to retrieve.
The legacy sourceIds field has been removed and maxChunks is now capped at 20 (previously 50). Sending sourceIds or any other extra key inside knowledge returns a 400 INVALID_ARGUMENT.

Response Descriptions

All fields are optional strings that instruct the AI on how to populate each finding field:
FieldDescription
summarySummary text of the overall analysis.
titleShort title of the finding.
descriptionDetailed description of the finding.
severitySeverity level. Values: critical, high, medium, low, info.
targetTextThe exact text on the page this finding refers to.
suggestionSuggested fix or recommendation.
isPageLevel"true" for visual/layout findings; "false" when targetText is real DOM text.
issueTypeIssue type for match-and-merge keying (short, lowercase, hyphenated).
confidenceConfidence score from 0 to 100.
htmlSelectorCSS selector of the DOM element.
findingTypeFinding type classification.

Example Requests

1. Minimal agent

{
  "data": {
    "name": "Simple Content Checker",
    "instructions": "Check for broken images and missing alt text on the page.",
    "contextGathering": {
      "strategies": ["web-page-html"]
    }
  }
}

2. Full custom agent with cross-page execution and Memory-RAG

{
  "data": {
    "name": "Brand Consistency Checker",
    "description": "Validates brand colors, logos, and typography across pages",
    "rawInstructions": "Check that all headings use the brand font 'Inter'. Verify the primary color #1A73E8 is used for CTAs.",
    "instructions": "Check that all headings use the brand font 'Inter'. Verify the primary color #1A73E8 is used for CTAs.",
    "enabled": true,
    "metadata": { "team": "growth", "owner": "jane" },
    "contextGathering": {
      "strategies": ["web-page-screenshot", "web-page-text", "web-page-html"],
      "strategyOptions": {
        "web-page-screenshot": { "scrollBeforeCapture": true }
      }
    },
    "execution": {
      "executionStrategy": "ai",
      "responseDescriptions": {
        "title": "Short name for the brand inconsistency",
        "severity": "One of: critical, high, medium, low",
        "suggestion": "Specific CSS or design fix",
        "issueType": "One of: brand-color, brand-font, brand-logo"
      },
      "knowledge": {
        "useMemory": true,
        "maxChunks": 10,
        "maxPatterns": 5,
        "maxActivities": 5
      }
    },
    "postProcess": {
      "guardrails": { "enabled": true },
      "matchAndMerge": { "enabled": true },
      "annotations": { "enabled": true, "strategy": "findings" },
      "analytics": { "enabled": true }
    },
    "input": {
      "inputRequirements": { "requires": ["url"] },
      "userContextFields": [
        { "id": "brand_color", "title": "Primary brand color?", "type": "string", "required": true },
        { "id": "brand_font", "title": "Heading font family?", "type": "string" }
      ]
    },
    "scope": {
      "pageScope": ["https://example.com/*"],
      "pageScopeExclude": ["https://example.com/admin/*"],
      "crossPage": {
        "enabled": true,
        "targetProperty": "brandConsistency",
        "pageDiscovery": "auto"
      }
    }
  }
}

3. Agent with live REST API context

{
  "data": {
    "name": "Pricing Parity Checker",
    "instructions": "Compare the price shown on the page with the price returned by the billing API and flag any mismatch.",
    "contextGathering": {
      "strategies": ["web-page-text", "rest-api"],
      "strategyOptions": {
        "rest-api": {
          "endpoints": [
            {
              "name": "Plan pricing",
              "description": "Canonical plan prices from the billing service",
              "url": "https://api.example.com/plans/{{variables.planId}}",
              "method": "GET",
              "headers": { "Accept": "application/json" },
              "auth": { "type": "bearer", "token": "sk_live_xxx" },
              "cacheTtlSeconds": 60,
              "responsePath": "data.plan"
            }
          ]
        }
      }
    },
    "input": { "variableKeys": ["planId"] }
  }
}

4. Agent that verifies content against an MCP server (mcp-tools)

{
  "data": {
    "name": "Docs Code Verifier",
    "instructions": "For every code snippet on the page, use the documentation MCP tools to find the matching feature and verify the snippet is correct and up to date. Flag any snippet that does not match the docs.",
    "contextGathering": {
      "strategies": ["web-page-html"]
    },
    "execution": {
      "executionStrategy": "mcp-tools",
      "mcpServers": [
        {
          "id": "velt-docs",
          "name": "Velt Docs MCP",
          "description": "Live Velt documentation MCP server",
          "url": "https://velt.dev/docs/mcp",
          "transport": "http",
          "auth": { "type": "none" }
        }
      ]
    }
  }
}

Response

Success Response

{
  "result": {
    "status": "success",
    "message": "Agent created successfully",
    "data": {
      "agentId": "abc123def456"
    }
  }
}
FieldTypeDescription
data.agentIdstringFirestore document ID of the new agent

Failure Response

{
  "error": {
    "message": "ERROR_MESSAGE",
    "status": "INVALID_ARGUMENT"
  }
}
Errors: INVALID_ARGUMENT (Zod validation failure — e.g. missing name, missing apiKey, a server-managed field, an unknown key inside knowledge, an invalid userContextFields.type, missing instructions for an AI/stagehand-agent/mcp-tools strategy, or missing/invalid execution.mcpServers when executionStrategy is "mcp-tools") / RESOURCE_EXHAUSTED (workspace already has the maximum number of custom agents).
{
  "result": {
    "status": "success",
    "message": "Agent created successfully",
    "data": {
      "agentId": "abc123def456"
    }
  }
}