Skip to main content
The CLI’s engini connect <app> wraps this whole page in one guided (and agent-resumable) command - see CLI commands. Use the SDK when you need the flow inside your own product.

Discover how an application connects

Every application lists its authentication methods - each with an authentication_id, a field list, and whether it’s OAuth:

Direct-credential connections (API keys, DB credentials…)

The OAuth flow

Three steps: get a sign-in URL, let the user authorize in a browser, then poll for the captured token and create the connection from it.
Pass the sign-in’s state as oauth_state (Python) / opts.oauthState (TypeScript) on create - the server looks up its own record of that sign-in and derives the connection’s credentials from it. Don’t copy connectionData into fields: values you send for server-derived keys (ApiKey, RefreshToken) are ignored, and ExternalOrgId is rejected outright with 400 INVALID_FIELD. Omitting oauth_state on an OAuth2 method returns 400 OAUTH_STATE_REQUIRED; a state the server can’t resolve - expired (past 5 minutes), unknown, or another account’s - returns 400 OAUTH_STATE_EXPIRED, deliberately identical for all three so it’s not an existence oracle. This applies to OAuth2 methods only - OAuth2WithoutSignIn (client-credentials) connectors need no state. The token record exists from the moment the sign-in URL is created, so every poll - including while the sign-in is pending - returns a body; while pending, status is an empty string and field_options/fieldOptions is {}. Poll on status, not on body presence: keep polling until status is success (or a failure status), not until the body is “no longer empty”. A null body means the same thing here as it does for create: the state can’t be resolved. It never turns non-null on a later poll, so treat it as a dead sign-in and start a new one - not as “still pending”.
Requires engini / @engini/sdk 0.21.0 or newer (0.20.0 and earlier cannot create OAuth2 connections - oauth_state/oauthState isn’t sent).

Field options discovered after sign-in

Some connection fields have no static list of legal values - the org itself has to be asked. Salesforce’s ApiVersionUrl is the example: it’s required, has no default, and the only source for it is the signed-in org’s own /services/data. The poll that ends the sign-in successfully (status is success, the same check as the polling loop above) carries a field_options / fieldOptions map for exactly this case, keyed by the field name:
field_options is an attrs model, not a dict. token.field_options supports "ApiVersionUrl" in fo and fo["ApiVersionUrl"], but has no .get() - use the in check above, not .field_options.get(...). Each value is an AccessTokenResponseFieldOptionsAdditionalProperty object; call .to_dict() on it to get the plain {key: label} dict shown in the comment.
The orientation is the load-bearing part. Each map is {valueToSave: labelToShow} - the key is what a create/update fields entry must send, the value exists only to show a human which one they’re picking. /services/data/v60.0 is the key; 60.0 is only the label. Sending the label instead of the key saves a connection that looks fine and fails on first use - ApiVersionUrl is a URL path prefix (SalesforceService.cs builds {ApiVersionUrl}/query?q=…), never a bare version string like v60.0 or 60.0.
field_options is populated only on the successful poll (status is success); on every other poll it’s {}. On the successful poll, an empty map ({}) - for a field, or for the whole response - means there’s nothing to offer: either no dynamic options exist for that field, or the lookup couldn’t complete. Either way, fall back to list_values, if any, or free text; don’t read {} on a pending poll as a lookup result at all.
Requires engini 0.24.0 or newer / @engini/sdk 0.24.0 or newer (earlier releases don’t declare field_options/fieldOptions on the access-token response at all).

Listing and filtering by kind

Every connection carries a kind - Regular, Key, or MCPClient. Filter the list to the subset the web UI’s Keys tab shows (see Keys vs Integrations):
Values are case-insensitive. MCPClient is accepted but returns nothing - this endpoint serves only Regular and Key connections. OPA is rejected with EnginiValidationError: on-prem agents are not connections on this surface. They have their own resource, client.opa, with their own lifecycle, token, settings, and logs.

Updating a connection

Change a name, description, credential fields, or metadata. Anything you leave out is omitted from the request, and the server leaves it unchanged.
fields and metadata replace the entire map - they do not merge. Rotating one credential by passing a single-entry fields clears every other field on the connection. Read the current values first and pass the full map.
Omitting a member is not the same as passing None/null: the SDKs leave an omitted member out of the request body entirely, so it is never mistaken for “clear this field”.

Re-authenticating an OAuth2 connection in place

update also takes oauth_state / oauthState - the state from a fresh get_sign_in_url sign-in. Passing it alone, with no other fields, is a valid edit: the server derives fresh credentials from that sign-in and applies them to the existing connection, so you never mint a new connection id or re-point workflows that reference it.
Required only when the sign-in should replace stored OAuth credentials (ApiKey, RefreshToken); everything else on update (renaming, editing other fields) needs no state. ExternalOrgId is rejected outright, with or without a state.

Pointing a workflow at a different connection

This is workflow-scoped, not a global swap - only workflow 903’s activities are rewired, and every other workflow using connection 481 is untouched. The arguments are keyword-only (Python) and an options object (TypeScript) on purpose: three same-typed integers are easy to transpose. Raises if an activity cannot be remapped, i.e. the target connection doesn’t expose the activity or object the workflow needs.

Refresh & object selection

Applications with supports_object_selection expose objects (tables, entities) to choose from. Refresh is asynchronous - trigger it, then poll:
select_objects replaces the whole selection set. A completed refresh also proves the connection authenticates - it’s a stronger health signal than check (which can be inconclusive). A refresh that doesn’t finish in time raises EnginiTimeoutError.

Defaults

The default connection is what implicit tool execution resolves to: