Skip to main content

Keychain Scopes Explained

NoETL keychain system supports three scope types that control how keychain entries are shared and accessed across executions.

Quick Reference

ScopeWho Can AccessLifetimeBest For
localSingle execution onlyUntil execution completesUser sessions, execution-specific tokens
globalAll executions of playbookUntil token expiresAPI keys, shared service tokens
sharedExecution tree (parent + children)Until root completesMulti-level orchestration

Scope Types

Local Scope (Execution-Scoped)

Isolation Level: Single execution
Cache Key: {name}:{catalog_id}:{execution_id}

Each execution gets its own isolated keychain entry. No sharing between executions - not even with child playbooks.

keychain:
- name: user_session
kind: oauth2
scope: local # Execution-scoped isolation
endpoint: https://auth.example.com/login
data:
username: "{{ workload.username }}"
password: "{{ workload.password }}"

Access Rules:

  • ✅ The execution that created it
  • ❌ Child playbooks (sub-playbooks)
  • ❌ Parent execution
  • ❌ Sibling executions
  • ❌ Other executions

When to Use:

  • User-specific authentication tokens
  • Execution-specific temporary credentials
  • Per-run isolated state
  • Testing with different credentials per execution

Example Scenario:

Execution 123: Creates local keychain "user_session" → Only 123 can access
Execution 456: Creates local keychain "user_session" → Only 456 can access
Both are completely isolated, even if they run the same playbook

Global Scope (Playbook-Wide)

Isolation Level: All executions of the playbook
Cache Key: {name}:{catalog_id}:global

Shared across all executions of the playbook. Most efficient for high-concurrency scenarios.

keychain:
- name: api_token
kind: oauth2
scope: global # Shared across all executions
auto_renew: true
endpoint: https://api.example.com/oauth/token

Access Rules:

  • ✅ All executions of this playbook
  • ✅ Concurrent executions
  • ✅ Past and future executions
  • ✅ Child playbooks (via inheritance)

When to Use:

  • Service-to-service authentication
  • API keys that don't change per execution
  • Rate-limited APIs (share token across executions)
  • High-concurrency workloads (avoid duplicate token requests)

Example Scenario:

Playbook: "data-sync" (catalog_id: 12345)
- Execution 100: Uses api_token (fetches it)
- Execution 101: Uses api_token (cached from 100)
- Execution 102: Uses api_token (cached from 100)
All three share the same token

Shared Scope (Execution Tree)

Isolation Level: Parent + all descendants
Cache Key: {name}:{catalog_id}:shared:{execution_id}

Accessible by the entire execution tree - parent and all child/grandchild playbooks.

keychain:
- name: orchestration_context
kind: http
scope: shared # Accessible by execution tree
endpoint: https://api.example.com/initialize
method: POST
data:
project_id: "{{ workload.project_id }}"

Execution Tree Example:

Playbook A (execution 100) → Creates shared keychain "orchestration_context"
├── Playbook B (execution 200) → Can access "orchestration_context"
│ └── Playbook C (execution 300) → Can access "orchestration_context"
└── Playbook D (execution 400) → Can access "orchestration_context"

All executions in tree can access the shared keychain

Access Rules:

  • ✅ Parent execution (creator)
  • ✅ Direct children (sub-playbooks)
  • ✅ All descendants (grandchildren, etc.)
  • ❌ Sibling execution trees
  • ❌ Unrelated executions

When to Use:

  • Multi-level playbook orchestration
  • Passing authentication context through call chain
  • Initialization state needed by sub-playbooks
  • Parent-child coordination

Example Scenario:

Main orchestrator playbook:
- Creates shared keychain with project context
- Calls data-ingestion playbook (child)
- Calls validation playbook (grandchild)
- All can access the project context from parent
- Calls reporting playbook (child)
- Can also access the project context

Separate orchestrator execution:
- Cannot access the other tree's shared keychain

Choosing the Right Scope

Use local when:

  • Each execution needs its own isolated credentials
  • Testing with different users/credentials
  • User-specific session tokens
  • Execution-specific temporary state

Use global when:

  • Token/credential is the same for all executions
  • High concurrency (share token to avoid rate limits)
  • Service-to-service authentication
  • Long-lived API keys

Use shared when:

  • Parent playbook calls sub-playbooks (orchestration)
  • Child playbooks need parent's authentication
  • Multi-level workflow coordination
  • Context passing through execution tree

Technical Details

Cache Key Formats

local:  {keychain_name}:{catalog_id}:{execution_id}
Example: "user_session:518486534513754563:518508477736551392"

global: {keychain_name}:{catalog_id}:global
Example: "api_token:518486534513754563:global"

shared: {keychain_name}:{catalog_id}:shared:{execution_id}
Example: "context:518486534513754563:shared:518508477736551392"

Database Schema

CREATE TABLE noetl.keychain (
cache_key TEXT PRIMARY KEY,
keychain_name TEXT NOT NULL,
catalog_id BIGINT NOT NULL,
scope_type TEXT NOT NULL CHECK (scope_type IN ('local', 'global', 'shared')),
execution_id BIGINT,
parent_execution_id BIGINT,
-- ... other fields
);

Valid scope_type values: local, global, shared


Common Patterns

Pattern 1: Global Service Token

keychain:
- name: api_token
kind: oauth2
scope: global
auto_renew: true
endpoint: https://api.example.com/oauth/token

workflow:
- step: fetch_data
tool:
kind: http
url: https://api.example.com/data
headers:
Authorization: "Bearer {{ keychain.api_token.access_token }}"

Pattern 2: Local User Session

workload:
username: "{{ env.USER }}"
password: "{{ env.PASSWORD }}"

keychain:
- name: user_session
kind: http
scope: local
endpoint: https://app.example.com/login
method: POST
data:
username: "{{ workload.username }}"
password: "{{ workload.password }}"

workflow:
- step: get_profile
tool:
kind: http
url: https://app.example.com/profile
headers:
Cookie: "session={{ keychain.user_session.session_id }}"

Pattern 3: Shared Orchestration Context

# Main orchestrator playbook
keychain:
- name: project_context
kind: http
scope: shared # Available to all sub-playbooks
endpoint: https://api.example.com/projects/initialize
method: POST
data:
project_id: "{{ workload.project_id }}"

workflow:
- step: run_ingestion
tool:
kind: playbook
path: data/ingestion
# Child can access {{ keychain.project_context }}

- step: run_validation
tool:
kind: playbook
path: data/validation
# Child can access {{ keychain.project_context }}

Migration Guide

If you have playbooks using invalid scope values:

Replace scope: execution with scope: local

# ❌ Before (invalid)
keychain:
- name: my_token
scope: execution

# ✅ After (correct)
keychain:
- name: my_token
scope: local # Execution-scoped

Replace scope: catalog with scope: global

# ❌ Before (invalid)
keychain:
- name: my_token
scope: catalog

# ✅ After (correct)
keychain:
- name: my_token
scope: global # Playbook-wide shared