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.
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
}
| Attribute | Field type | Result when not set |
|---|---|---|
| Required Secret attribute | OctoSecretState! | { "isSet": false, "keyMissing": false, "setAt": null } |
| Optional Secret attribute | OctoSecretState | Same 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.
| Input | Effect |
|---|---|
| Non-empty string | Sets or rotates the secret (the server encrypts it) |
Field omitted, null, "" | Unchanged |
Echoed read object ({ isSet }) or secretIsSet in the generic input | Unchanged |
Name listed in clearSecretAttributes | Cleared (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.ClearSecretAttributescarries the same list.
Querying
| Operation | Secret attributes |
|---|---|
| Field filter | Only IS_NULL and IS_NOT_NULL ("is set" = the field exists and is not null) |
| Sort | Refused |
| Attribute (text) search | Refused |
| Aggregation, group-by | Refused |
| Query columns, persistent-query row cells | Refused |
| Archive column paths, CrateDB columns | Not available |
Count the entities with a configured secret:
query {
runtime {
systemCommunicationEMailSenderConfiguration(
fieldFilter: [{ attributePath: "password", operator: IS_NOT_NULL }]
) {
totalCount
}
}
}
Error Codes
| Code | When | Details |
|---|---|---|
SecretAttributeNotQueryable | A filter other than IS_NULL / IS_NOT_NULL, sort, search, aggregation, group-by, query column or query-row cell on a Secret attribute | extensions.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 input | Message numbers in OctoDetails as "NN: text" — never values |
SecretEncryptionNotConfigured | The 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):
| Role | Grants |
|---|---|
AdminPanelManagement | Secrets overview, sweep reports and the list of sweep runs |
SecretManagement | Starting 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:
| Operation | clientSecret |
|---|---|
GET (list and by id), POST and PUT responses | Always null (write-only). The PUT response is built from the stored entity, not echoed |
POST | Required, non-empty |
PUT | Non-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):
| Field | Meaning |
|---|---|
clientSecretIsSet: boolean | A readable client secret is stored |
clientSecretKeyMissing: boolean | A 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: falseandkeyMissing: true(generic:secretIsSet: false,secretKeyMissing: true); the secrets overview reports the formKEY_MISSINGand 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 sweepCleanupUnreadable. 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!]!
}
| Form | Meaning |
|---|---|
NOT_SET | No value |
PLAINTEXT | Legacy clear text still stored (before the Encrypt sweep) |
ENC_V1 | Legacy enc:v1 value (instance key) |
ENC_V2 | Protected, key id known |
KEY_MISSING | Protected, key id not in this environment's key ring |
CORRUPT | Stored 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
ExportRtand deep-graph exports never contain Secret values (ownershipSecret).- 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 inredactedPaths(seeRevealSecret@1). - MCP (tool reference): every tool that returns entities shows
value: nullplussecretIsSet.create_entityandupdate_entity(medium risk) refuse a non-empty Secret value;null,""or an echoed{ "isSet": … }leave the secret unchanged, andupdate_entitytakesclearSecretAttributes. Setting or rotating a secret is done with the high-risk toolset_entity_secrets; an entity whose type has a required secret is created with the high-risk toolcreate_entity_with_secrets. Query and aggregation tools refuse Secret attributes withSecretAttributeNotQueryable.get_secret_statusandstart_secret_sweepcover the encryption status and the sweep. - octo-cli:
SecretStatus,ReprotectSecretsandDeleteSecretSweepDump(see Secret Encryption Key Ring).
See Also
- Secret Attributes — the value type and its compiler rules
- Update — general update semantics
- Secret Encryption Key Ring — operations