Skip to content

Runtime context ​

The ctx object every action, source and hook receives: HTTP, SQL, mail, state, logging and errors, with the limits Console enforces.

Enterprise module

Connectors are the enterprise connectors module. A workspace without the module gets 402 from the connectors API.

Your code runs in a separate process and has no network of its own. Everything that leaves the process goes through ctx, and Console applies the rules below to each call. The credential is added by Console, so the package never reads a token, key or password.

What ctx holds ​

MemberWhat it is
ctx.settingsThe connector's settings, read-only. Fields that are hidden or not valid on this deployment are undefined
ctx.deployment'saas' or 'onprem'
ctx.locale'en' or 'tr'
ctx.connector{ id, tenantId, projectId, packageVersion }
ctx.account{ id, label, captured } of the connected account, or null. captured holds the non-secret values your identity hook or an OAuth capture returned
ctx.httpOutside requests. See ctx.http
ctx.sqlRead-only SQL. Only in a package that declares sql. See ctx.sql
ctx.mailSending mail. Only in a package that declares mail. See ctx.mail
ctx.state, ctx.cacheSmall key-value stores. See State and cache
ctx.logdebug, info, warn and error. See Logging
ctx.signalAn AbortSignal that fires when Console cancels the call
ctx.now()The current time as an ISO 8601 string
ctx.t(key, vars?)A message in the caller's locale; {name} placeholders are replaced from vars

ctx.sql and ctx.mail throw POLICY_DENIED in a package that does not declare the matching block.

ctx.http ​

ts
const repo = await ctx.http.get<Repo>(p`/repos/${owner}/${repo}`)
const res = await ctx.http.request<Page>({ method: 'POST', path: '/search', json: { q } })

ctx.http.get(path, query?) and ctx.http.post(path, json?) return the parsed body. ctx.http.request(req) returns { status, headers, data, url }. A response with status 400 or above throws a ConnectorError, see Errors.

Request memberMeaning
methodGET, HEAD, POST, PUT, PATCH or DELETE. Anything else is refused with POLICY_DENIED
pathRelative to http.baseUrl, starting with /. Build it with the p tag
urlAn absolute URL instead of path. Its host must be allowed by permissions.egress
queryValues are strings, numbers, booleans or lists. undefined and null are skipped
headersExtra headers
json, form, multipart, bodyThe body. Give one of them. body is a string, bytes or an async iterable; multipart is a list of parts
contentTypeContent type for body
authDefault true. false sends the request without the account's credential
responseTypejson, text, bytes or stream. By default a JSON content type is parsed
timeoutMs, maxBytesPer-request overrides of the timeout and the response size cap
retryfalse to never retry, or { idempotencyKey } to allow retrying a POST
mapErrorPer-request override of http.mapError; false turns it off

Build paths with p`/repos/${owner}/${repo}`. The p tag encodes each value as one path segment and throws INVALID_INPUT for a value that is . or .., contains /, \, an encoded slash or a control character. Use p.path(value) for a value that spans segments, such as a file path.

Rules Console applies ​

RuleValue
EgressEvery request, and every redirect hop, must target a host allowed by permissions.egress. Otherwise the call fails with EGRESS_DENIED
Address checkConsole resolves the name and connects to the addresses that passed. Private addresses are reachable only for a server the operator entered, on-prem, with privateNetwork: 'onprem'
CredentialAdded only for the origin of http.baseUrl and the origins in the method's inject.origins. Dropped on a redirect to another origin
URLs with a user name or passwordRefused
RedirectsFollowed up to 5 hops, each checked. A package with http.profile receives the 3xx answer instead
TimeoutAt least 1 000 ms and at most 60 000 ms per request. The default is the package's http.timeoutMs, or 30 000 ms
Response sizeThe default cap is http.maxResponseBytes, or 10 MiB. Console's ceiling is 25 MiB. A larger response fails with UPSTREAM
Request bodyA body larger than four times the response cap is refused with INVALID_INPUT
Headers the package cannot setHost, Connection, Content-Length, Transfer-Encoding, Keep-Alive, Upgrade, TE, Trailer, Expect, Proxy-Authorization, Proxy-Connection. A User-Agent is added when you set none
Plain httpAccepted for a typed endpoint on-prem only. On SaaS the value is refused
Read actionsGET and HEAD, plus the http.readPost and http.graphql paths. A method-override header (X-HTTP-Method-Override, X-HTTP-Method, X-Method-Override) is refused. Failures are POLICY_DENIED

A request is also bounded by the call's own deadline: it never runs past the time the call has left.

Retries ​

Retries are off unless http.retry is set.

MemberMeaning
attemptsTotal attempts, clamped to 1 to 10
onResponse statuses that are retried
methodsMethods that may be retried. Default GET, HEAD, PUT, DELETE
respectRetryAfterWait as long as the Retry-After header says. A wait over 30 seconds gives up

A POST or PATCH is retried only when the request carries retry: { idempotencyKey }; Console sends the key as an Idempotency-Key header. Transport failures that surface as UPSTREAM or TIMEOUT are retried too. Without Retry-After the wait starts at 500 ms, doubles with each attempt up to 8 seconds, and adds up to 250 ms of jitter. A retry never starts if it would pass the call's deadline.

If a request made with an OAuth account that has a refresh token gets HTTP 401, Console renews the token once and sends the same request again. Do not retry a 401 yourself.

ctx.sql ​

A package that reads a database declares sql and calls ctx.sql. The administrator enters the server in the connector's settings and the database user and password in the account. Console opens the connection itself, so the package never sees the password. Every call is read-only whatever the action's access.

ts
const r = await ctx.sql.query({ sql: 'select id, name from people where id = $1', params: [id], maxRows: 50 })
return { people: rowsAsObjects(r), truncated: r.truncated ?? null }
MethodReturns
query({ sql, params?, maxRows?, maxBytes?, maxCellBytes?, timeoutMs? }){ columns, rows, rowCount, truncated?, cutColumns?, warnings?, elapsedMs }. rows are arrays in column order; rowsAsObjects(result) turns them into objects
schemas()List of schema names
tables({ schema?, search?, kind?, limit? }){ tables, truncated? }; each table is { schema, name, kind: 'table' | 'view', rowEstimate?, comment? }
describe({ schema?, table })Columns, indexes and foreign keys
explain({ sql, params? }){ plan }, without running the statement
ctx.sql.engineThe engine in use: postgres, mysql or mariadb

Placeholders are the engine's own: $1 for PostgreSQL, ? for MySQL and MariaDB. Console does not rewrite the statement. params are positional: a string, a finite number, a boolean or null. Pass a bigint or a date as a string. The catalog calls (schemas, tables, describe) are built by Console with bound parameters; never build such a query from model input yourself.

Limits:

LimitValue
Rows per result100 by default, at most 50 000 (maxRows)
Result size256 KiB by default, at most 8 MiB of JSON (maxBytes)
Text cell8 KiB by default, at most 1 MiB (maxCellBytes). Columns with a cut cell are listed in cutColumns
Statement time15 s by default, at most 60 s (timeoutMs)
Pooled connections3 by default, at most 10 per replica (poolMax)
Statement text1 to 20 000 characters
ParametersAt most 100, 64 KiB of text in all
Names (schema, table, search)At most 256 characters, no control characters
Tables per tables call200 by default, at most 1 000
Data read from the server per statementFour times maxBytes plus 1 MiB, at most 32 MiB
Failed connection attempts6 a minute per connector, 20 a minute per workspace

A call and the package's sql.limits can only lower a maximum. Ask for the least you need. A call returns one result; a source that reads a large table pages with repeated calls using a key, not an offset.

Values are JSON. Integers beyond 2^53-1 and DECIMAL values are strings. NaN and Infinity are the strings "NaN" and "Infinity". A date is YYYY-MM-DD, a time HH:MM:SS[.fff], a timestamp without zone YYYY-MM-DDTHH:MM:SS[.fff], and a timestamp with zone is ISO 8601 in UTC. json is parsed. Binary is { "$binary": "<base64 of the first 256 bytes>", "bytes": <size> }. A column's type is one of int, bigint, decimal, float, bool, text, date, time, timestamp, timestamptz, interval, uuid, json, binary, other.

Read-only comes in layers. Console starts the session read-only where the engine allows it, refuses a statement that is not a single read statement before it borrows a connection (including MySQL and MariaDB optimizer hints and sequence functions such as NEXTVAL), and sets a server-side time limit. A database account that can write still connects, so make identity report it and tell the administrator in the guide to create a read-only account. No column is masked: a column the account can read, the package can read.

Errors are ConnectorErrors. Every failure to reach the server is UPSTREAM with the same text, "The database server could not be reached." Too many failed connection attempts give RATE_LIMITED with retryAfterSec. A request outside the bounds above fails with INVALID_INPUT before it leaves the package.

On SaaS the port is the engine's default (5432, 3306) or any port from 1024 up, except a list of well-known non-database ports. On-prem any port is allowed. verify-full and verify-ca work everywhere; skip-verify and disable are on-prem only.

ctx.mail ​

A package that sends mail declares mail and calls ctx.mail. The administrator enters the mail server, the security mode, the sender and optional limits as settings, and the account in a mail account method. Console builds the message, connects and signs in, so the package never sees the password.

ts
const r = await ctx.mail.send({ to: ['ada@example.com'], subject: 'Hello', text: 'Hi Ada' })
// { messageId, accepted, rejected, sizeBytes, serverReply }
MethodWhat it does
send(message)Sends one message. Returns { messageId, accepted, rejected, sizeBytes, serverReply }. Console never retries once the server has started to receive the body
preview(message)Builds and checks the message with the same code as send and connects to nothing. Returns the exact headers, the recipient list the server would get, the body, the size and warnings. Use it for an action that only shows a draft, marked access: 'read'
verify()Connects, signs in and quits. Returns { server, tls, tlsVersion?, auth, extensions, maxSizeBytes? }, never a secret. Use it in identity

A message is { to, cc?, bcc?, subject, text, html? }. An address is 'a@example.com' or { address, name? }. A message has no sender, reply-to, header, attachment, envelope or in-reply-to member: all of those come from the settings, and a message that carries one is refused.

Message rules, as exported in MAIL_LIMITS:

RuleValue
Recipients (to, cc, bcc together)1 to 25 (recipients). The connector's own setting may lower the cap; the default is 10 (defaultRecipients)
Subject1 to 200 characters (subject), no line break or control character
Text bodyAt most 100 000 characters (text)
HTML bodyAt most 100 000 bytes (htmlBytes)
BodyAt most 100 000 bytes (bodyBytes)
Whole messageAt most 1 048 576 bytes (rawBytes)
Display nameAt most 100 characters (displayName), none of <, >, "
AddressAt most 254 characters (address); the part before the @ at most 64 (localPart)
Sends per connector10 a minute (perMinute) and 60 an hour (perHour)
Duplicate windowAn identical message (same recipients, subject, text and html) within 60 seconds (dedupeSec) fails with CONFLICT

Console also limits a workspace's mail connectors together to 30 messages a minute and 300 an hour, and a connector's allowed recipient domains to 50.

An address must be a plain local@domain. The part before the @ may hold letters, digits and . _ + -. No quoted local part, comment, group, display name or [ip] literal is accepted. A recipient is checked against the connector's allowed domains and recipient cap, and recipients are de-duplicated across the three lists without regard to case. bcc never appears in a header.

html is optional simple HTML. It is refused, never cleaned, unless it uses only these tags: p br h1-h6 ul ol li a strong b em i u blockquote code pre hr table thead tbody tr th td span div. The only attribute is href on a, in quotes, starting with https: or mailto:. Styles, comments, CDATA, images, any other tag or attribute, and a < that does not start one of these tags (write &lt;) are refused. The error names the first problem.

Errors are INVALID_INPUT (the message), POLICY_DENIED (a recipient domain or count the settings do not allow), AUTH_FAILED, UPSTREAM (the server refused or failed), TIMEOUT, RATE_LIMITED (with retryAfterSec) and CONFLICT (duplicate). The plain security mode and a relay without sign-in are available on-prem only; on SaaS Console connects to ports 465, 587 and 2525.

Object storage (signing) ​

Amazon S3, services that speak its protocol, and Azure Blob Storage do not take a bearer token: every request is signed or carries a shared access signature. The package never holds the secret and never signs. It declares what the credential is made of, and Console signs each ctx.http request that carries the credential. Set engines.console to a Console version that has the signing operations, or an older Console would refuse to install the package.

The signing inject kinds are in Connector definition reference. They need these http members:

MemberMeaning
http.profiles3 or azure-blob. Console checks the shape of every request: which method goes with the service, bucket or object level, which query names are allowed, which vendor headers may be sent. Anything else is refused with POLICY_DENIED and the rule's name
http.methodsRequired with a profile. A non-empty list from GET, HEAD, PUT, POST, PATCH, DELETE; a verb outside it is refused whatever the action does
http.scope{ fromSettings } naming a tags setting with the buckets or containers the connector may reach. When it is not empty, a request whose first path segment is not in the list is refused, and so is a request for the whole account

A package with a profile cannot also declare readPost or graphql, and never follows a redirect. It receives the 3xx answer, so a wrong-region answer can become a clear error.

What the package does:

  • Never sets Authorization, Date, x-amz-date, x-amz-content-sha256, x-amz-security-token or x-ms-date. Console removes them and sets its own. A signature or SAS token in the query is refused.
  • Builds the absolute url itself, with every path segment and query value encoded to the strict RFC 3986 set (A-Za-z0-9-._~, everything else %XX in upper case), and passes no query and no p-built path: the signature covers the bytes that are sent.
  • Sends a body whose bytes it can hold in memory. Console signs the real SHA-256 of the body.
  • Reads the answer's XML itself; the SDK adds no XML library.
  • Treats its own bucket list as an extra check. The boundary is the credential's own policy and http.scope; the guide should tell the administrator to scope the credential too.

Request shapes the s3 profile accepts:

Level and methodQuery names allowed
Service GETmax-buckets, continuation-token, prefix, bucket-region
Bucket GETlist-type (2), prefix, delimiter, max-keys, continuation-token, start-after, encoding-type (url), fetch-owner
Bucket HEADnone
Object GET, HEAD, PUTnone

Request shapes the azure-blob profile accepts:

Level and methodQuery names allowed
Service GETcomp (list), prefix, marker, maxresults, timeout
Container GETrestype (container), comp (list), prefix, delimiter, marker, maxresults, timeout
Container HEADrestype (container), timeout
Blob GET, HEAD, PUTtimeout

A PUT may set content-type only to text/plain, text/markdown, text/csv or application/json, and may not set content-disposition or content-encoding. A request that carries no signature may only be GET or HEAD.

State and cache ​

ts
const seen = (await ctx.state.get<number>('count')) ?? 0
await ctx.state.set('count', seen + 1)
await ctx.cache.set('token-info', info, 300) // key, value, ttl in seconds
StoreMethodsLimits
ctx.stateget(key), set(key, value), delete(key)A value is at most 64 KB of JSON; a larger value fails with POLICY_DENIED
ctx.cacheget(key), set(key, value, ttlSec)An entry expires after ttlSec seconds

Both are scoped to the workspace and the connector. Keep values small, and write code that still works when a value is missing, because the store lives with the package process.

Logging ​

ts
ctx.log.info('Fetched page', { page: 2, items: 30 })

ctx.log.debug, info, warn and error take a message and an optional object. Console cuts a message at 2 000 characters, writes it to its log, and masks every secret value of the call, including their encodings, before it is written. Do not log credentials on purpose; the masking is a safety net, not a licence.

Errors ​

Throw a ConnectorError to fail a call with a clear reason:

ts
import { ConnectorError } from '@cognipeer/connector-sdk'

throw new ConnectorError({
  code: 'NOT_FOUND',
  message: 'No such document.',
  hint: 'List the documents first and use an id from the list.',
})

ConnectorError takes code, message, and optionally hint, retryAfterSec and details (strings and numbers). The model sees the code, the hint and the message, so write them for it: say what went wrong and what to do next.

CodeUse it when
AUTH_FAILEDThe credential is rejected or expired. The account needs to sign in again
FORBIDDENThe account is valid but may not do this
NOT_FOUNDThe thing does not exist
INVALID_INPUTThe input is wrong in a way the model can fix
CONFLICTThe state changed or the request duplicates another
RATE_LIMITEDThe service asks the caller to slow down. Set retryAfterSec
UPSTREAMThe service failed or answered something unexpected
TIMEOUTThe service did not answer in time

Console raises four more codes itself. Your code sees them as exceptions and should not throw them.

CodeMeaning
RUNTIME_UNAVAILABLEThe package process is down, the call was cancelled, or this Console does not have the host operation
EGRESS_DENIEDA request went to a host the package did not declare
POLICY_DENIEDA rule refused the call: a read action that tried to write, a request outside a profile, a mail recipient outside the allowed domains, ctx.sql without sql
APPROVAL_REQUIREDUsed by the test host when a write action runs without approved

How an error surfaces: Console masks secrets in the message, cuts it to 300 characters, and marks text that came from a vendor response as vendor text. An exception that is not a ConnectorError reaches the caller as a generic "The connector failed unexpectedly."; the stack stays in the package's log.

Mapping vendor errors ​

ctx.http turns an HTTP status of 400 or above into a ConnectorError. Without http.mapError the mapping is:

StatusCode
400, 422INVALID_INPUT
401AUTH_FAILED
403FORBIDDEN
404, 410NOT_FOUND
409, 412CONFLICT
429RATE_LIMITED, with retryAfterSec from Retry-After when it is a number
408, 504TIMEOUT
otherUPSTREAM

The default message is <host> returned HTTP <status>. Give the model more with http.mapError, which runs for every response that is not a stream, 2xx included. Return an error to fail the call, or undefined to accept the response (or fall back to the default for a failing status):

ts
http: {
  baseUrl: 'https://api.docs.example.com/v2',
  mapError(res) {
    const body = res.data as { error?: { message?: string } } | undefined
    const message = body?.error?.message ?? `Docs returned HTTP ${res.status}.`
    switch (res.status) {
      case 401: return new ConnectorError({ code: 'AUTH_FAILED', message, hint: 'Reconnect the account.' })
      case 429: return new ConnectorError({ code: 'RATE_LIMITED', message, retryAfterSec: Number(res.headers['retry-after'] ?? 30) })
      default: return undefined
    }
  },
},

Retry is decided after mapError, so the statuses in retry.on are retried even when you map them.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub