Building an Action Provider
Author a provider with ProviderSpec and RegisterFromSpec, write an action's Execute function, declare input/output schemas, and wire it via fx.
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
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
Registerfunction 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
Here is a complete crypto provider example. It runs in process and does not
require authentication:
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 ActionRegistrations pairing each ActionSpec
with its Execute function.
The ProviderSpec
ProviderSpec (application/connector/provider_spec.go) is the declarative input
to RegisterFromSpec:
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_callaction automatically whenBaseURL != "", so an HTTP provider gets a raw pass-through action even if you declare no explicit actions. - Derives the fully-qualified
ActionKeys/TriggerKeys/ModelKeys/MapperKeysfrom the registered specs. - Synthesizes a
TestAuthhook fromAuthorVerificationwhen you don't supply one, so "test connection" works out of the box.
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 ActionRegistrations only for the operations you want to
expose as first-class, schema-validated actions.
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):
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).
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.
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
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 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:
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_codeThe 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.
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 ActionSpecs —
the same RegisterFromSpec call sits at the bottom of both.
Where to go next
- Architecture — how your action is dispatched and its semantics enforced.
- Authentication — the auth types your provider can declare.
- Triggers & Polling — adding event sources to your provider.
The Connector Catalog
The generated provider inventory, parity matrix, categories and release stages, colocated provider layout, and code generators.
Assisted Action Authoring
An AI-assisted, governed workbench for authoring connector functions — draft, compile, dry-run, repair, and stage a deployment proposal without bypassing certification or promotion gates.