Secrets Management
Loopstack provides built-in tools for requesting and retrieving secrets (API keys, tokens, etc.) from users at runtime.
Overview
Secrets are requested from the user via a SecretRequestDocument form (or RequestSecretsTask in agent loops) and persisted in the database via SecretEntity, scoped per workspace. Values are never exposed to the LLM — only key names and availability flags (GetSecretKeysTool) are returned to workflow code.
Providing Secrets to Remote Environments
When a workflow runs commands on a remote sandbox or Fly.io machine via @loopstack/remote-client-module, secrets must reach that environment to be useful. The SyncSecretsTool (sync_secrets) reads all workspace secrets, ships them to the remote agent over its authenticated control channel, and writes them as .env variables before restarting the app. Values stay on the server side of the control channel and are not surfaced to the LLM.
Call sync_secrets before launching long-running commands or whenever a secret changes:
constructor(private readonly syncSecrets: SyncSecretsTool) { super();}
@Transition({ from: 'secrets_received', to: 'ready' })async pushSecrets(state: SecretsState) { await this.syncSecrets.call({});}Available Tools
| Tool | Source | Description |
|---|---|---|
RequestSecretsTask |
@loopstack/secrets |
Agent-friendly task that launches a secrets sub-workflow |
GetSecretKeysTool |
@loopstack/secrets |
List stored secret keys and their availability |
SecretRequestDocument |
@loopstack/secrets |
Document displaying the secret input form |
Example Workflow
import { BaseWorkflow, Transition, Workflow } from '@loopstack/common';import { MarkdownDocument } from '@loopstack/common';import { GetSecretKeysTool, SecretRequestDocument } from '@loopstack/secrets';
interface SecretsState { secretKeys?: Array<{ key: string; hasValue: boolean }>;}
@Workflow({ widget: './secrets-example.ui.yaml' })export class SecretsExampleWorkflow extends BaseWorkflow { constructor(private readonly getSecretKeys: GetSecretKeysTool) { super(); }
@Transition({ to: 'requesting_secrets' }) async requestSecretsFromUser(state: SecretsState) { await this.documentStore.save(SecretRequestDocument, { variables: [{ key: 'EXAMPLE_API_KEY' }, { key: 'EXAMPLE_SECRET' }], }); }
@Transition({ from: 'requesting_secrets', to: 'verifying', wait: true }) async secretsSubmitted(state: SecretsState) { const result = await this.getSecretKeys.call({}); this.assignState({ secretKeys: result.data }); }
@Transition({ from: 'verifying', to: 'end' }) async showResult(state: SecretsState) { await this.documentStore.save(MarkdownDocument, { markdown: this.render(join(__dirname, 'templates', 'secretsVerified.md'), { secretKeys: state.secretKeys, }), }); }}How It Works
- Request — saving a
SecretRequestDocumentwith the needed keys shows a secure input form in the UI - Wait — The workflow pauses on a
wait: truetransition namedsecretsSubmitted(the transition the form fires) until the user submits the secrets - Verify —
GetSecretKeysToolchecks which secrets are now stored - Use — Secrets are available as environment variables in subsequent tool calls
Template Example
# Secrets Verification
{{#each secretKeys}}
- **{{this.key}}**: {{#if this.hasValue}}Stored{{else}}Missing{{/if}} {{/each}}Registry References
- Secrets examples — Request secrets from user, verify storage, and display results with both direct workflow and agent-based approaches