Skip to main content

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).

  1. Open Admin > Tenant Settings > API Keys and select Create API key.
  2. 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.
  3. 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:

PermissionAllowsDefault roles
custom_connector.readRead connectors, versions and exportsowner, admin, operator, auditor
custom_connector.modifyCreate, edit, import, fork, duplicate and delete; test drafts of connectors that run in Owlie's cloud; check conformanceowner, admin, operator
custom_connector.publishPublish a version; test drafts of connectors that run on a gateway (LDAP, SQL), which runs unpublished code on your networkowner, admin
custom_connector.installInstall a version as an integration, together with integration.modifyowner, admin
integration.modifyInstall and upgrade integrationsowner, admin, operator
integration.readOpen 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​

EndpointUse
POST https://<your Owlie domain>/api/graphqlConnectors, versions, drafts, import, export, publish, install
POST https://<your Owlie domain>/api/rpcRunning a draft's handlers and checking conformance
https://<your Owlie domain>/api/mcpThe 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_writes has no effect. custom_connector.modify is enough.
  • LDAP and SQL connectors run on your gateway, and so does an HTTP connector whose execution_targets is ["gateway"]. The run needs custom_connector.publish, and credentials typed into integration_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 except connection.validateConfig and connection.validateStatements, reads included. The result carries no logs, blocked_egress or attempted_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​

  1. Research and design (steps 2 to 5 of the main path), and write the design table into README.md.
  2. Create or import the connector, then write the draft.
  3. Read the returned issues; fix them until there are none.
  4. Run connectors.runTest (HTTP) or connectors.testArtifact for each case in Test your connector, against a test account, and save the inputs in fixtures/inputs.json so a person can rerun them in the builder.
  5. Run connectors.checkConformance, then publish, or hand the draft to a person to publish.
  6. Report what you tested live, what you did not, and what remains an assumption.