Skip to main content

Secret Attributes in the API

This page describes how the asset repository GraphQL API, the identity-provider REST API, MCP and octo-cli handle attributes of value type Secret. The rule in one sentence: you can write a secret, and you can see whether it is set — no API returns the value.

Availability

Available from System CK model 2.5.0 and the engine and asset repository release that ships the Secret value type. The CK model API reports the value type as SECRET.

Reading​

Typed fields​

A Secret attribute is exposed as a field of type OctoSecretState instead of String:

type OctoSecretState {
isSet: Boolean! # false when not set OR when the stored value cannot be read
keyMissing: Boolean! # true: a value is stored, but its key id is not in this environment's key ring
setAt: DateTime # when the current value was set (UTC); null when not set or set before this feature
}
AttributeField typeResult when not set
Required Secret attributeOctoSecretState!{ "isSet": false, "keyMissing": false, "setAt": null }
Optional Secret attributeOctoSecretStateSame object — the field is nullable in the schema but always returns an object

keyMissing: true (together with isSet: false) marks a value that is stored encrypted with a key the environment does not have, typically after a restore from another environment. See Unreadable secrets.

query {
runtime {
systemCommunicationEMailSenderConfiguration(rtId: "65f1c0a2b3d4e5f601234567") {
items {
rtId
name
userName
password {
isSet
keyMissing
setAt
}
}
}
}
}
{
"rtId": "65f1c0a2b3d4e5f601234567",
"name": "Notifications",
"userName": "service@example.com",
"password": { "isSet": true, "keyMissing": false, "setAt": "2026-10-06T08:15:00Z" }
}

Existing documents that select a Secret field as a scalar (password without a sub-selection) fail GraphQL validation. This is intended: a client that expects a string fails loudly instead of receiving a value.

Generic projection​

The generic attribute projection returns value: null for a Secret attribute and reports the state in secretIsSet, secretKeyMissing and secretSetAt. All three are null for every attribute that is not Secret.

type RtEntityAttribute {
attributeName: String
value: SimpleScalar # always null for a Secret attribute
secretIsSet: Boolean # null for non-secret attributes, true/false for secrets
secretKeyMissing: Boolean # null for non-secret attributes; true when stored but unreadable (unknown key id)
secretSetAt: DateTime # null for non-secret attributes, legacy values and unset secrets
}
query {
runtime {
runtimeEntities(ckId: "System.Communication/EMailSenderConfiguration", rtId: "65f1c0a2b3d4e5f601234567") {
items {
attributes {
items {
attributeName
value
secretIsSet
}
}
}
}
}
}
{ "attributeName": "password", "value": null, "secretIsSet": true }

Sending such a projection back in an update is safe — it means "unchanged" (see below).

Writing​

The input type of a Secret attribute stays String (typed inputs: password: String; record members: token: String). Every non-empty input is encrypted with the active key before it is stored; an encrypted value can therefore never be copied from one entity to another through the API.

InputEffect
Non-empty stringSets or rotates the secret (the server encrypts it)
Field omitted, null, ""Unchanged
Echoed read object ({ isSet }) or secretIsSet in the generic inputUnchanged
Name listed in clearSecretAttributesCleared (error if the secret is required)
Inside records: member omitted, null or ""Carried over from the stored element with the same record key

The generic input RtEntityAttributeInput { attributeName, value, secretIsSet } accepts secretIsSet and ignores it, so echoing back what was read is safe. Creating an entity requires a value for every required Secret attribute. A stored but unreadable secret (keyMissing) counts as present for this check.

Placeholders have no meaning: a string such as <client-secret> or TODO_SET_CLIENT_SECRET sent through any API is an ordinary value and is encrypted like any other. Only the one-time migration of legacy clear text treats such strings specially (see Sweep).

Setting or rotating a secret​

mutation {
runtime {
systemCommunicationEMailSenderConfigurations {
update(entities: [{ rtId: "65f1c0a2b3d4e5f601234567", item: { password: "example-password" } }]) {
rtId
password {
isSet
}
}
}
}
}

Other secrets of the entity stay untouched. The mutation result follows the reading rules: it returns { isSet }, never the value.

Clearing a secret​

Because null means "unchanged", clearing is explicit. clearSecretAttributes: [String!] (camelCase attribute names) sits on the update entry — next to rtId and item, not inside item. Both the typed <Type>InputUpdate and the generic RtEntityUpdate have it:

mutation {
runtime {
systemCommunicationEMailSenderConfigurations {
update(entities: [{ rtId: "65f1c0a2b3d4e5f601234567", item: {}, clearSecretAttributes: ["password"] }]) {
rtId
password {
isSet
}
}
}
}
}
  • Clearing a required Secret attribute is an error.
  • Setting and clearing the same secret in one update is an error.
  • The SDK's MutationDto.ClearSecretAttributes carries the same list.

Querying​

OperationSecret attributes
Field filterOnly IS_NULL and IS_NOT_NULL ("is set" = the field exists and is not null)
SortRefused
Attribute (text) searchRefused
Aggregation, group-byRefused
Query columns, persistent-query row cellsRefused
Archive column paths, CrateDB columnsNot available

Count the entities with a configured secret:

query {
runtime {
systemCommunicationEMailSenderConfiguration(
fieldFilter: [{ attributePath: "password", operator: IS_NOT_NULL }]
) {
totalCount
}
}
}

Error Codes​

CodeWhenDetails
SecretAttributeNotQueryableA filter other than IS_NULL / IS_NOT_NULL, sort, search, aggregation, group-by, query column or query-row cell on a Secret attributeextensions.attributePath, extensions.operation
ASSET1004 (validation)Required secret missing on create (message 2), clearing a non-secret or unknown name (21), clearing a required secret (22), set and cleared together (23), required secret missing after record carry-over (24), non-string inputMessage numbers in OctoDetails as "NN: text" — never values
SecretEncryptionNotConfiguredThe server has no key ring—

Permissions​

Data permissions stay type-level row filters:

  • Read access on an entity shows whether its secrets are set.
  • Writing a secret requires write access on the entity.
  • No permission grants decryption through the public API. There is no decrypt endpoint.

Two tenant roles govern secret administration (the initial tenant administrator holds both):

RoleGrants
AdminPanelManagementSecrets overview, sweep reports and the list of sweep runs
SecretManagementStarting sweeps and deleting a pre-sweep dump early (see Sweep). Seeded by System.Identity.Bootstrap; constant CommonConstants.SecretManagementRole

Secrets are decrypted only inside trusted services: the communication controller when it builds adapter configuration, and the mesh adapter when a pipeline runs RevealSecret@1. Every decryption is counted (octo.secrets.decrypt); logs never contain the value.

Secrets in Records​

A record array is written as a whole. On update and replace, at any nesting depth, a Secret member that is omitted, null or "" in an incoming element is carried over from the stored element with the same record key (the record type's recordKey). A single Record attribute carries over by position. Keys compare by value (integers across numeric types, otherwise ordinal text). A record array without a key (models from before the recordKey rule) gets no carry-over.

Example: the stored value has two elements with keys smtp and imap; the update sends:

[
{ "path": "smtp", "secretValue": null },
{ "path": "imap", "secretValue": "example-password-1" },
{ "path": "pop3", "secretValue": "example-password-2" }
]

Result: smtp keeps its stored secret, imap gets the new secret, pop3 is added. An element that is no longer sent is removed together with its secret. A Secret member inside a record cannot be cleared on its own — clearSecretAttributes names top-level attributes only; drop the element to remove it, or send a new value to replace it.

In the projection, the Secret member of a record element shows { isSet } like a typed field (endpoints { key label token { isSet } }).

Identity Providers (REST)​

The identity service stores the ClientSecret of OAuth providers as a Secret attribute (System.Identity 2.23). For GoogleIdentityProviderDto, MicrosoftIdentityProviderDto, FacebookIdentityProviderDto and AzureEntraIdProviderDto on {tenantId}/v1/identityProviders:

OperationclientSecret
GET (list and by id), POST and PUT responsesAlways null (write-only). The PUT response is built from the stored entity, not echoed
POSTRequired, non-empty
PUTNon-empty rotates; null, "" or omitted keep the stored secret. It cannot be cleared (the secret is required)

A placeholder-looking value (<…>, TODO_SET_*) is an ordinary client secret. The responses carry the state instead of the value (each field omitted when null):

FieldMeaning
clientSecretIsSet: booleanA readable client secret is stored
clientSecretKeyMissing: booleanA client secret is stored, but its key id is not in the key ring (clientSecretIsSet is then false). The provider is skipped at sign-in with an error log until the secret is entered again
clientSecretSetAt: string (ISO-8601)When the current client secret was set; null for legacy values

octo-cli -c UpdateIdentityProvider and the MCP tool update_identity_provider send a client secret only when a new one is given (see Identity Providers).

Unreadable Secrets (Key Missing)​

A protected value whose key id is not in the environment's key ring — typically after restoring a dump from another environment or a child tenant — stays stored encrypted:

  • Readers see isSet: false and keyMissing: true (generic: secretIsSet: false, secretKeyMissing: true); the secrets overview reports the form KEY_MISSING and lists the value as a re-entry task.
  • Services that need the value (adapter configuration, RevealSecret@1, identity providers, AI services) treat it as not set and log an error without the value.
  • It becomes readable again automatically as soon as its key id is added to the key ring.
  • It is removed only by entering a new value (re-entry), by clearing it with clearSecretAttributes (optional secrets) or by the admin sweep CleanupUnreadable. No API decrypts or exports plaintext.

A stored value that cannot be parsed at all reads as not set as well (form CORRUPT, warning logged).

Secrets Overview (Admin GraphQL)​

The asset repository offers a tenant-wide overview of all Secret attributes, including identity-provider client secrets (System.Identity/*) and AI credentials (System.Ai/*). It requires the tenant role AdminPanelManagement (otherwise error code Forbidden) and never returns values. There is no child-tenant aggregation: query each child tenant's endpoint.

type Query { secrets: SecretsQuery }

type SecretsQuery {
inventory(
first: Int = 50, after: String,
ckTypeId: String, # exact CK type (derived types included)
forms: [SecretStorageForm!],
needsReEntry: Boolean, # only re-entry tasks
search: String # rtId, rtWellKnownName, display name, attribute path (never values)
): SecretInventoryConnection!
summary: SecretInventorySummary!
usages(ckTypeId: String!, rtId: OctoObjectId!, attributePath: String!): [SecretUsage!]!
}

enum SecretStorageForm { NOT_SET, PLAINTEXT, ENC_V1, ENC_V2, KEY_MISSING, CORRUPT }

type SecretInventoryItem {
ckTypeId: String!
rtId: OctoObjectId!
rtWellKnownName: String
displayName: String
attributePath: String! # camelCase; record members as "endpoints[key=prod].token" / "credentials.token"
attributeName: String! # CK attribute name of the top-level attribute
required: Boolean!
form: SecretStorageForm!
keyId: String # ENC_V2 / KEY_MISSING only
setAt: DateTime
needsReEntry: Boolean! # KEY_MISSING or CORRUPT, or NOT_SET and required
usedBy: [SecretUsage!]!
}

type SecretUsage {
dataFlowRtId: OctoObjectId
dataFlowName: String
pipelineRtId: OctoObjectId!
pipelineName: String
nodePath: String!
match: SecretUsageMatch! # EXACT: RevealSecret@1 with ckTypeId + rtId + attributeName; BY_TYPE: rtIdPath on the same type and attribute
}

type SecretInventorySummary {
total: Int!, notSet: Int!, plaintext: Int!, encV1: Int!, encV2: Int!, keyMissing: Int!, corrupt: Int!, needsReEntry: Int!
encV2ByKeyId: [KeyIdCount!]!
}
FormMeaning
NOT_SETNo value
PLAINTEXTLegacy clear text still stored (before the Encrypt sweep)
ENC_V1Legacy enc:v1 value (instance key)
ENC_V2Protected, key id known
KEY_MISSINGProtected, key id not in this environment's key ring
CORRUPTStored value cannot be parsed (reads as not set)

Re-entry tasks are live: inventory(needsReEntry: true). A task is done when a new value is set (→ ENC_V2), the optional secret is cleared, or the CleanupUnreadable sweep removed it.

query {
secrets {
summary { total keyMissing needsReEntry encV2ByKeyId { keyId count } }
inventory(needsReEntry: true, first: 20) {
totalCount
items { ckTypeId rtId displayName attributePath form keyId setAt }
}
}
}

Exports, Backups, MCP and octo-cli​

  • ExportRt and deep-graph exports never contain Secret values (ownership Secret).
  • Backups (dumps) contain the encrypted values; a restore keeps them encrypted (see Unreadable secrets and Restore across environments).
  • Pipeline debug snapshots mask revealed values as *** and list the masked locations in redactedPaths (see RevealSecret@1).
  • MCP (tool reference): every tool that returns entities shows value: null plus secretIsSet. create_entity and update_entity (medium risk) refuse a non-empty Secret value; null, "" or an echoed { "isSet": … } leave the secret unchanged, and update_entity takes clearSecretAttributes. Setting or rotating a secret is done with the high-risk tool set_entity_secrets; an entity whose type has a required secret is created with the high-risk tool create_entity_with_secrets. Query and aggregation tools refuse Secret attributes with SecretAttributeNotQueryable. get_secret_status and start_secret_sweep cover the encryption status and the sweep.
  • octo-cli: SecretStatus, ReprotectSecrets and DeleteSecretSweepDump (see Secret Encryption Key Ring).

See Also​