Build connectors with the API and agents
Everything the Connector Builder does, a script or an AI agent can do through Owlie's API: create a connector, write its files, test a draft, publish, install and export. This page lists the operations in the order you use them. The design steps are the same as in the main path; only the tools differ.
Authenticate
API requests authenticate with an API key, sent as Authorization: Owlie <key> (or
Authorization: Bearer <key>). An API key belongs to a service account, and it can do exactly what
that service account's roles allow.
Get an API key
Creating a key needs the api_key.modify permission (the owner and admin roles have it by default).
- Open Admin > Tenant Settings > API Keys and select Create API key.
- Enter a Service account name. Owlie creates a new service account with that name, and the key belongs to it. An expiry date is optional.
- Select Create API key, and copy the key from API key created. Owlie shows it only once.
Through the API, createAPIKey(input: { service_account_name: "connector-agent", expires_at: null })
returns token (the key) and apiKey { identity_id } (the new service account).
A new service account has no roles. Give it the roles it needs: open the role in Admin > Directory >
Roles and assign it to the service account's identity, or call
assignRole(roleId: "<role id>", principalId: "<identity_id>", principalType: IDENTITY), which needs
role.modify. These permissions matter for connectors:
| Permission | Allows | Default roles |
|---|---|---|
custom_connector.read | Read connectors, versions and exports | owner, admin, operator, auditor |
custom_connector.modify | Create, edit, import, fork, duplicate and delete; test drafts of connectors that run in Owlie's cloud; check conformance | owner, admin, operator |
custom_connector.publish | Publish a version; test drafts of connectors that run on a gateway (LDAP, SQL), which runs unpublished code on your network | owner, admin |
custom_connector.install | Install a version as an integration, together with integration.modify | owner, admin |
integration.modify | Install and upgrade integrations | owner, admin, operator |
integration.read | Open an Owlie connector's source (View source) | owner, admin, operator, auditor |
Give an agent's service account only what the agent needs. A service account without
custom_connector.publish can build HTTP and SCIM drafts and test them, and a person publishes them.
Testing an LDAP or SQL draft needs custom_connector.publish.
Endpoints
| Endpoint | Use |
|---|---|
POST https://<your Owlie domain>/api/graphql | Connectors, versions, drafts, import, export, publish, install |
POST https://<your Owlie domain>/api/rpc | Running a draft's handlers and checking conformance |
https://<your Owlie domain>/api/mcp | The Model Context Protocol server, for AI agents |
Your Owlie domain is the host name you sign in to Owlie at. All three endpoints take the same API key. Every GraphQL type and field is in the schema reference.
The TypeScript SDK, @owlieapp/sdk, wraps the GraphQL operations:
import { OwlieSDK } from "@owlieapp/sdk";
const owlie = new OwlieSDK({ apiKey: process.env.OWLIE_API_KEY!, tenantDomain: "acme.owlie.example" });
const connectors = await owlie.custom_connectors.list();
Its custom_connectors methods are list, get, get_version, create, update, update_draft,
publish_version, delete_connector, export_connector, import_connector, get_system_source,
create_fork, duplicate, resolve_fork_conflicts, rebase_fork and flatten_fork.
1. Create the connector
Either create an empty connector and write its files, or import a complete pack in one call.
mutation {
createCustomConnector(input: { key: "acme-projects", name: "Acme Projects", kind: "http" }) {
id
draft {
id
draft_revision
}
}
}
kind is http, scim, ldap, sql or flat-file, and cannot change later.
mutation ($document: JSONObject!) {
customConnectorImport(
input: { document: $document, target: { create: { key: "acme-projects", name: "Acme Projects" } }, dry_run: true }
) {
ok
issues {
code
requirement
message
path
}
connector_id
draft_revision
digest
unchanged
}
}
document is a pack. With dry_run: true the import validates and writes
nothing; drop it to write. To import over an existing draft instead, use
target: { replace_draft: { connector_id, expected_revision } }.
2. Write the draft
mutation ($files: JSONObject!, $manifest: JSONObject) {
updateCustomConnectorDraft(
connectorId: "<connector id>"
versionId: "<draft version id>"
input: { expected_revision: 3, manifest_json: $manifest, files: $files }
) {
draft_revision
issues {
code
requirement
message
path
}
}
}
files replaces the draft's whole file map, keyed by path, so send every file each time.
expected_revision must equal the draft's current draft_revision. If someone changed the draft
since you read it, the call fails with CUSTOM_CONNECTOR_REVISION_CONFLICT and writes nothing: read
the draft again, merge, and retry. For an engine-based kind, write definition.json in files; Owlie
derives the manifest. The returned issues are the same problems the builder shows
(Validation issues).
When the draft is a fork, also pass expected_base_digest in input (and in an import's
replace_draft target): the fork { base { digest } } of the version you read. If the base changed
since, the call fails with CUSTOM_CONNECTOR_BASE_CHANGED and writes nothing, because the content you
edited is stale. Read the draft again and retry.
3. Test a draft
Draft tests run on POST /api/rpc, with a body of { "procedure": "<name>", "input": { ... } }.
Every run is live: it builds the draft and calls the real system, so point it at a test account.
Which procedure you call depends on where the connector runs.
HTTP connectors that run in Owlie's cloud run through connectors.runTest, which takes its
credentials from the Run config saved with the draft, never from the request. Save the Run config
through updateCustomConnectorDraft (its test_run_config_json input, write-only), read the hosts a
run would reach with connectors.previewTestHosts, then send them back as confirmed_hosts:
{
"procedure": "connectors.runTest",
"input": {
"connector_id": "<connector id>",
"version_id": "<draft version id>",
"manifest_json": { "...": "the manifest to test" },
"files": { "handlers/user/fetchAll.ts": "..." },
"artifact_key": "user.fetchAll",
"test_input": { "pageSize": 10 },
"confirmed_hosts": ["api.acmeprojects.example"],
"idempotency_key": "<a new unique value for this run>"
}
}
The response is a receipt: { "result": { receipt_id, status, hosts, blocked_hosts, result, ... } }. If the
hosts changed since you previewed them, nothing runs and result is
{ "status": "hosts_changed", "hosts", "ignored" }. Test your connector
describes both calls in full. connectors.testArtifact refuses an HTTP connector's draft.
SCIM, LDAP and SQL connectors, and an HTTP connector limited to a gateway, run through
connectors.testArtifact:
{
"procedure": "connectors.testArtifact",
"input": {
"connector_id": "<connector id>",
"version_id": "<draft version id>",
"artifact_key": "user.fetchAll",
"test_input": { "pageSize": 10 },
"integration_config": {
"config": { "subdomain": "acme-sandbox" },
"auth": { "key": "api-token", "config": { "api_token": "<test token>" } }
}
}
}
The response is { "result": { ok, data, error, logs, blocked_egress, attempted_hosts, duration_ms } }.
files and manifest_json in the input test unsaved changes without saving them to the draft; for an
engine-based kind Owlie derives the manifest from definition.json and ignores manifest_json.
The Run config you save in the builder holds test credentials, so the GraphQL API never returns it:
no connector or version field carries it, and an API key, the MCP server or an agent cannot read it.
Only the web builder reads it back. For connectors.testArtifact, send the configuration a run needs in
integration_config.
What else a run needs depends on where the connector runs:
- HTTP and SCIM connectors run in Owlie's cloud. Every handler runs as soon as you call it, writes
included, and
allow_writeshas no effect.custom_connector.modifyis enough. - LDAP and SQL connectors run on your gateway, and so does an HTTP connector whose
execution_targetsis["gateway"]. The run needscustom_connector.publish, and credentials typed intointegration_config(a gateway secret name is refused). Add"gateway_id"to choose the gateway; without it, Owlie uses your one connected gateway. A handler that can change the system runs only with"allow_writes": true. On a SQL connector that is every handler exceptconnection.validateConfigandconnection.validateStatements, reads included. The result carries nologs,blocked_egressorattempted_hosts.
connectors.checkConformance with { "connector_id", "version_id" } answers
{ "result": { ok, required, issues } }. It checks the manifest and the handler set as publishing
does. Publishing also checks the files and compiles them, so a draft can pass this check and still fail
to publish.
A flat-file connector answers only connection.validateConfig and connection.testConnection; its
files are uploaded with an upload token instead (Upload files and run ingestion).
Test your connector lists the cases to run, and
Saved inputs the format of fixtures/inputs.json.
Errors
A GraphQL call that Owlie refuses answers with an entry in errors: its message says what failed,
extensions.code names the kind of error (such as ValidationError, KEY_TAKEN or
CUSTOM_CONNECTOR_REVISION_CONFLICT) and extensions.status the HTTP status it stands for. A refusal
with several problems lists them in extensions.context.issues, each with code, message and
path, and a publish whose files do not compile lists each error in
extensions.context.build_errors. The requirement strings of
Validation issues are not part of a GraphQL error; match on
extensions.code and read the message. Calls that complete return their issues in the result itself.
A call on /api/rpc that Owlie refuses answers an HTTP error status with this body:
{
"error": {
"code": "INVALID_REQUEST",
"message": "\"user.create\" can change the system it connects to; confirm to run this unpublished draft against it (allowWrites)",
"details": { "requirement": "draft.allow-writes", "connector_id": "<connector id>" }
}
}
code follows the status: INVALID_REQUEST (400), UNAUTHORIZED (401), FORBIDDEN (403, a missing
permission), NOT_FOUND (404), CONFLICT (409) and RATE_LIMITED (429). A test-run refusal carries
details.requirement: draft.allow-writes, draft.gateway (with a gateways list to choose from
when you have several), draft.gateway-allowlist or draft.secret-ref
(Validation issues). A handler that runs and fails is not
an error: the call answers 200 with ok: false and the failure in error.
4. Publish and install
mutation {
publishCustomConnectorVersion(versionId: "<draft version id>", connectorId: "<connector id>", expectedRevision: 4) {
published {
id
version_number
}
new_draft {
id
draft_revision
}
}
}
mutation {
installIntegration(input: { catalog_source: custom, catalog_key: "<connector id>", catalog_version: 1 }) {
id
}
}
Installing needs custom_connector.install and integration.modify. Upgrading an installed
integration, or moving it back to an earlier version, is reconcileIntegrationManifest with
target_catalog_version (Publish, install and upgrade).
5. Export
query {
customConnectorExport(connectorId: "<connector id>", version: 1) {
document
digest
}
}
Leave out version to export the draft. A fork exports as one complete pack.
Through MCP
An AI agent connected to Owlie's MCP server builds connectors with the owlie_execute tool, which
runs a short TypeScript program against the same operations. The connector methods are
custom_connectors_list, custom_connectors_get, custom_connector_versions_get,
custom_connectors_create, custom_connectors_update, custom_connectors_update_draft,
custom_connectors_publish_version, custom_connectors_export, custom_connectors_import,
custom_connectors_duplicate, custom_connectors_get_system_source, custom_connectors_create_fork,
custom_connectors_resolve_fork_conflicts, custom_connectors_rebase_fork,
custom_connectors_flatten_fork and custom_connectors_delete.
Changes an agent makes through MCP run with its API key's permissions, immediately. The service
account's roles are the control: leave out custom_connector.publish if a person should publish.
MCP has no method for test runs or conformance checks. An agent working through MCP calls
connectors.runTest (or connectors.testArtifact for the other kinds) and connectors.checkConformance
on /api/rpc with the same API key.
A loop for agents
- Research and design (steps 2 to 5 of the main path), and write the design table into
README.md. - Create or import the connector, then write the draft.
- Read the returned
issues; fix them until there are none. - Run
connectors.runTest(HTTP) orconnectors.testArtifactfor each case in Test your connector, against a test account, and save the inputs infixtures/inputs.jsonso a person can rerun them in the builder. - Run
connectors.checkConformance, then publish, or hand the draft to a person to publish. - Report what you tested live, what you did not, and what remains an assumption.