# Building an Action Provider (/docs/connectors/building-a-provider)



Adding a connector is a matter of describing a provider and its actions, then
registering it — no changes to the core engine. This page shows the declarative
path (`ProviderSpec` + `RegisterFromSpec`), how to author an action, and how the
generated catalog is produced.

## The two authoring paths [#the-two-authoring-paths]

There are two ways a provider enters the registry, both funneling through the
same `Registrar`:

* **Declarative / generated** — most catalog providers are described in a
  bootstrap manifest and their registration code is generated. Good for HTTP APIs
  that fit the standard shape.
* **Hand-written Go** — a small `Register` function for providers that need
  custom logic (in-process computation, bespoke webhook handling). This is the
  path you'll use for a one-off provider in your own build.

Both call `providerkit.RegisterFromSpec`
(`infrastructure/connector/providerkit/register_from_spec.go`).

## A minimal provider [#a-minimal-provider]

Here is a complete `crypto` provider example. It runs in process and does not
require authentication:

```go
func Register(r appconnector.Registrar, _ *Client) error {
	return providerkit.RegisterFromSpec(r, nil, appconnector.ProviderSpec{
		Meta: dc.Provider{
			Key:          "crypto",
			DisplayName:  "Crypto",
			Description:  "Generate random passwords, hash text, sign with HMAC, ...",
			DocsURL:      docsURL,
			Categories:   []dc.Category{dc.CategoryDevTools},
			Version:      "0.1.0",
			AuthTypes:    []dc.AuthType{dc.AuthNone},
			Publisher:    identity.ConnectorProviderKey,
			ReleaseStage: dc.ReleaseStageBeta,
		},
		BaseURL: "",
		Auth:    nil,
		Hooks: dc.ProviderHooks{
			TestAuth: func(_ context.Context, _ *dc.Connection) error { return nil },
		},
		Actions: []appconnector.ActionRegistration{
			{Spec: cryptoactions.HashTextAction, Execute: cryptoactions.ExecuteHashText},
			{Spec: cryptoactions.HMACSignatureAction, Execute: cryptoactions.ExecuteHMACSignature},
			// ... more actions
		},
	})
}
```

That's the whole contract: a `Meta` describing the provider, an optional `Auth`,
optional `Hooks`, and a list of `ActionRegistration`s pairing each `ActionSpec`
with its `Execute` function.

## The ProviderSpec [#the-providerspec]

`ProviderSpec` (`application/connector/provider_spec.go`) is the declarative input
to `RegisterFromSpec`:

```go
type ProviderSpec struct {
	Meta             dc.Provider          // ActionKeys/TriggerKeys left empty; derived at register
	BaseURL          string               // API root, may contain ${...} templates
	Auth             *APIKeyAuth          // nil for OAuth2 / no-auth
	Actions          []ActionRegistration
	Triggers         []TriggerRegistration
	Models           []dc.ModelSpec
	Mappers          []dc.MapperSpec
	ObjectCatalogs   []dc.ObjectCatalogDeclaration
	CatalogDiscovery []dc.CatalogDiscoveryDeclaration
	Hooks            dc.ProviderHooks     // TestAuth synthesized from Auth when nil
	Verification     *VerificationSpec    // replaces the synthetic GET {BaseURL}/me probe
	ProxyHeaders     map[string]string    // provider-default headers for generic_api_call
	ProxyQuery       map[string]string
}

type APIKeyAuth struct {
	PrimaryKey string   // credential key, e.g. "apiKey" or dc.CredKeyAccessToken
	ExtraKeys  []string // additional required credential keys
}

type VerificationSpec struct {
	Method          string            // default "GET"
	Endpoints       []string          // first 2xx = pass
	Headers         map[string]string // static, non-secret only
	BaseURLOverride string
}
```

`RegisterFromSpec` does several things for you:

* Validates the spec against the provider policy.
* **Appends a `generic_api_call` action automatically when `BaseURL != ""`**, so
  an HTTP provider gets a raw pass-through action even if you declare no explicit
  actions.
* Derives the fully-qualified `ActionKeys` / `TriggerKeys` / `ModelKeys` /
  `MapperKeys` from the registered specs.
* Synthesizes a `TestAuth` hook from `Auth` or `Verification` when you don't
  supply one, so "test connection" works out of the box.

<Callout type="info" title="An HTTP provider can be nearly zero-code">
  Set `BaseURL` and an `Auth` key and you have a working provider with a
  `generic_api_call` action and a synthesized auth test — no `Execute` functions
  required. Add named `ActionRegistration`s only for the operations you want to
  expose as first-class, schema-validated actions.
</Callout>

## Authoring an action [#authoring-an-action]

An action pairs an `ActionSpec` (the description) with an `ActionExecuteFunc`
(the work). Here is the `crypto.hash_text` action
(`infrastructure/connector/providers/crypto/actions/hash_text.go`):

```go
var hashTextOutputSchema = json.RawMessage(`{"type":"string"}`)

var HashTextAction = dc.ActionSpec{
	Key:                  "crypto.hash_text",
	Provider:             "crypto",
	DisplayName:          "Text to Hash",
	Description:          "Converts text to a hash value using the chosen algorithm.",
	Auth:                 dc.AuthNone,
	InProcess:            true,
	DefaultTimeout:       5 * time.Second,
	DefaultFailurePolicy: dc.FailureFail,
	OutputSchema:         hashTextOutputSchema,
	Properties: []dc.Property{
		{
			Name: "method", Label: "Method", Type: dc.PropStaticDropdown, Required: true,
			StaticOptions: []dc.PropertyOption{
				{Label: "MD5", Value: "md5"},
				{Label: "SHA256", Value: "sha256"},
			},
		},
		{Name: "text", Label: "Text", Type: dc.PropShortText, Required: true},
	},
	Annotations: dc.ActionAnnotations{ReadOnly: true, Idempotent: true},
}

func ExecuteHashText(_ context.Context, in dc.ActionInput) (dc.ActionOutput, error) {
	method := stringArg(in.Args, "method")
	text := rawStringArg(in.Args, "text")
	// ... compute the digest ...
	raw, _ := json.Marshal(digest)
	return dc.ActionOutput{Result: raw}, nil
}
```

An `Execute` reads typed arguments from `in.Args`, uses `in.Auth` for
credentials (nil here since auth is `none`), and returns an `ActionOutput`. For
actions that mutate downstream state, return an `EffectPatch` and set the action's
`Semantics` so the engine knows the effect class, idempotency kind, and retry
safety (see [Architecture](/docs/connectors/architecture)).

<Callout type="warn" title="Set semantics on anything that writes">
  A read action can leave semantics at their defaults, but a `create`, `update`,
  `delete`, or `send` action **must** declare its `ActionSemantics`. The registry
  validates them at startup — a not-idempotent action marked `safe_retry`, or a
  destructive action without an approval requirement, fails registration rather
  than silently double-writing in production.
</Callout>

## Declaring input and output schemas [#declaring-input-and-output-schemas]

Input is authored as `[]Property` (`domain/connector/property.go`), *not* raw
JSON Schema — Ductor renders the JSON Schema (Draft 2020-12) for you via
`RenderActionSchema`. A `Property` has a `Name`, `Label`, `Type`, `Required`,
optional `StaticOptions` or a `DynamicResolver`, nested `Properties` for objects
and arrays, and a `Validation` block (min/max length, pattern, enum, numeric
bounds).

`PropertyType` has 19 values, including
`short_text`, `long_text`, `number`, `checkbox`, `datetime`, `json`, `object`,
`array`, `static_dropdown`, `dropdown` (dynamic), `multi_select`, `secret`,
`markdown`, and `connection`. Widget hints are emitted under `x-ductor-*` schema
extensions so a generic JSON Schema validator ignores them.

The `OutputSchema` is passed through verbatim as a raw `json.RawMessage` — you
write the JSON Schema for the result yourself.

## Registering via fx [#registering-via-fx]

A hand-written provider is wired by invoking its `Register` in the `fx` graph.
Generated providers are wired automatically. If you're adding a provider to your
own build, add an `fx.Invoke(yourprovider.Register)` (and an `fx.Provide` for its
client if it has one) alongside the connector registry module.

## The generated catalog [#the-generated-catalog]

The bulk of the catalog is code-generated from a declarative manifest — you won't
usually write this, but understanding it helps when reading the catalog source.
The manifest (`cmd/ductor/connector_bootstrap_manifest_*.yaml`, schema
`connector_bootstrap_manifest.schema.json`) declares each provider:

```yaml
version: 2
providers:
  - key: absorb_lms
    name: AbsorbLms
    import_path: github.com/ductor-io/ductor/infrastructure/connector/providers/absorb_lms
    capabilities:
      supports_oauth: true
      supports_refresh: false
      supports_shared_webhook: false
      supports_poll_triggers: false
    provider:
      display_name: Absorb LMS
      release_stage: beta
      categories: [other]
      auth_types: [oauth2, none]
      base_url: https://${connectionConfig.portalRoute}
      oauth2:
        auth_url: https://${connectionConfig.portalRoute}/oauth/authorize
        token_url: https://${connectionConfig.portalRoute}/oauth/token
        grant_type: authorization_code
```

The generator (`internal/cmd/connectorbootstrapgen`) reads every manifest file
and emits chunked `cmd/ductor/fx_connector_generated_*.go` files. For each
provider it emits a client constructor, a `register_<key>` function that calls
`providerkit.RegisterFromSpec` with the inlined spec, and the `fx` wiring.
Validate a manifest with `make connector-manifest-validate`.

<Callout type="info" title="Manifest for the standard shape, Go for the exceptions">
  If your provider is a normal HTTP API with a standard auth type, a manifest
  entry is the least code. If it needs in-process logic, custom hooks, or bespoke
  action bodies, write a `Register` function and hand-author the `ActionSpec`s —
  the same `RegisterFromSpec` call sits at the bottom of both.
</Callout>

## Where to go next [#where-to-go-next]

* [Architecture](/docs/connectors/architecture) — how your action is dispatched and its semantics enforced.
* [Authentication](/docs/connectors/authentication) — the auth types your provider can declare.
* [Triggers & Polling](/docs/connectors/triggers-and-polling) — adding event sources to your provider.
