Constants and Secrets
Constants and secrets solve the same broad problem:
how should a Main Sequence workflow receive configuration at runtime?
They are both small pieces of runtime configuration, but they are not interchangeable.
Use the split below:
Constant: safe operational configurationSecret: sensitive configuration
If leaking the value would create an incident, it should not be a constant.
Quick Decision Rule
Use a Constant for things like:
- feature flags
- model windows and thresholds
- dataset names
- vendor identifiers
- small JSON mappings
- mode switches such as
production,paper, orsandbox
Use a Secret for things like:
- API keys
- bearer tokens
- passwords
- webhook signing secrets
- connection credentials
- private vendor access tokens
In practice:
MODEL__DEFAULT_WINDOW = 252is a constantFEATURE__ENABLE_VALIDATION = trueis a constantPOLYGON_API_KEY = "..."is a secretAWS_SECRET_ACCESS_KEY = "..."is a secret
Mental Model
Think of constants and secrets as runtime inputs, not workflow outputs.
Jobs, resources, APIs, agents, and notebooks often need configuration that should not live directly inside code. Constants and secrets give you a place to store that configuration in the platform.
A useful mental split is:
- code defines behavior
- constants define non-sensitive runtime settings
- secrets define protected credentials
That keeps the repository cleaner and reduces the amount of environment-specific data hardcoded into scripts and jobs.
Constants and Secrets are owned by an Organization Environment. SDK operations use the current registered branch's Environment context, with authenticated runtime target checks when deployed. A branch without an Environment raises only when a scoped operation needs one. Users do not pass an Environment UID or branch UID.
Names Are Unique Identities
Treat constant and secret names as Environment-level unique configuration keys.
In practice, this means:
MODEL__DEFAULT_WINDOWshould identify one constant, not several variantsPOLYGON_API_KEYshould identify one secret, not several conflicting rows- name-based reads such as
Constant.get(name=...)orSecret.get(name=...)assume uniqueness
For idempotent automation or setup flows:
- first resolve whether the constant or secret already exists by name
- use
get(name=...)when you expect exactly one object - use
filter(name__in=[...])when reconciling a batch of keys - create only the missing names
Do not treat create(...) as a substitute for existence checks when the task is "make sure this value exists".
RBAC and Shareable Resources
Constants and secrets sit inside a broader platform access model.
Main Sequence applies resource-level access control, so the real operational question is not only "what is this value?" but also:
- who can see it
- who can change it
- which team or CodeRepository boundary it belongs to
- whether it is safe to expose through a deployed release
Constants and secrets are decided by their policies:
- a
Constant's policies arereader,editorandowner; see Who can use a constant - a
Secret's policies arereader,value_reader,editorandowner, and seeing a secret does not show its value; see Who can use a secret
Every other shareable resource is decided by its policies too: a Bucket and
an Artifact, see Artifacts; a
CodeRepository, its branches and Projects, see
Code Repositories, Branches and Projects; an Agent,
see Agents; and a ResourceRelease and a Job, see
Who can use a release and
Who can use a Job.
Why these matter:
CodeRepositorydefines an important collaboration and execution boundaryConstantstores shareable, non-sensitive runtime configurationSecretstores protected credentialsBucketholds artifacts and files that may themselves contain sensitive or controlled contentArtifactis the individual stored object that may need controlled visibilityResourceReleaseis the deployment-facing wrapper for supported resources such as APIs
IMPORTANT
Constant and Secret are not the only shareable resources in the SDK.
Teams, Organization Environments, Buckets, Artifacts, code repositories, their branches, Projects,
Agents, resource releases and Jobs are decided by policies, as Constant and Secret are, and so
are model-provider credentials and custom model providers, which have no SDK model; see
Who Has Access to an Object.
This guide focuses on constants and secrets because they are the simplest place to learn the policy pattern.
That is the practical reason this topic matters early:
- constants and secrets teach the access-control model in its simplest form
- the same idea scales later to CodeRepositories, buckets, artifacts, and deployed resources
Constants
The client model is:
from mainsequence.client import Constant
A constant is a small, readable, organization-level configuration value.
The current SDK model exposes:
uidnamevaluecategory, the prefix of the name before__organization_environment_uidandorganization_environment_name, the Environment that owns it
What constants are good for
Good fits:
- default windows such as
252 - tuning parameters such as confidence thresholds
- external dataset names
- reusable small lookup dictionaries
- environment-independent configuration values
Bad fits:
- passwords
- API keys
- secrets copied from other systems
- anything that should not be casually displayed in a UI, notebook, or log
Naming and categories
Constants are expected to use UPPER_SNAKE_CASE.
You can also group constants with a double underscore:
ASSETS__MASTER
MODEL__DEFAULT_WINDOW
FEATURE__ENABLE_VALIDATION
BROKER__DEFAULT_ACCOUNT
This does not create a nested object. It is just a naming convention.
Main Sequence treats the prefix before __ as a human-readable category. For example:
ASSETS__MASTERis displayed under categoryASSETSMODEL__DEFAULT_WINDOWis displayed under categoryMODEL
This is useful when you want the constants table to stay readable without inventing a more complex hierarchy.
Constant examples
Simple scalar value:
Constant.create(
name="MODEL__DEFAULT_WINDOW",
value=252,
)
Boolean flag:
Constant.create(
name="FEATURE__ENABLE_VALIDATION",
value=True,
)
Small JSON object:
Constant.create(
name="BROKER__DEFAULTS",
value={
"execution_venue": "paper",
"account_type": "margin",
},
)
Reading constants
Read one constant:
from mainsequence.client import Constant
default_window = Constant.get(name="MODEL__DEFAULT_WINDOW").value
Read just the value:
default_window = Constant.get_value("MODEL__DEFAULT_WINDOW")
Read multiple constants by name:
constants = Constant.filter(
name__in=[
"MODEL__DEFAULT_WINDOW",
"FEATURE__ENABLE_VALIDATION",
]
)
Current client-side supported filters are:
namename__in
For idempotent constant setup, prefer resolving by name before create:
existing = {c.name for c in Constant.filter(name__in=["MODEL__DEFAULT_WINDOW"])}
if "MODEL__DEFAULT_WINDOW" not in existing:
Constant.create(name="MODEL__DEFAULT_WINDOW", value=252)
Who can use a constant
A constant's access is decided by its policies:
reader: see the constant and its value;editor: also change its name and value, delete it, and see who has access;owner: also decide who has access.
The person or workload that creates a constant holds owner, and Organization
admins hold every action. The policies go to people, workload identities and
Teams. Read and change them with the access methods on Constant:
from mainsequence.client import Constant
constant = Constant.get(name="MODEL__DEFAULT_WINDOW")
for binding in constant.get_access().bindings:
print(binding.uid, binding.policy.id, binding.principal.type, binding.principal.uid)
binding = constant.attach_policy("reader", principal_type="team", principal_uid=team_uid)
constant.detach_policy(binding.uid)
answer = constant.explain_access("constants.update", principal_uid=user_uid)
print(answer.allowed, answer.granting_policies)
The access methods address the constant by its UID alone, so they need no
Environment. A refused change raises PermissionDeniedError, whose payload
names the required_action and the granting_policies.
mainsequence access policies --type constants lists the policies with their
actions, and Who Has Access to an Object describes every
method and model.
When to prefer a constant over code
Use a constant instead of hardcoding when:
- operators may need to change the value without editing source
- the value is reused across several jobs or scripts
- the value is environment or organization specific
- the value is business configuration, not program logic
Examples:
- a rebalance threshold
- a default benchmark name
- a vendor dataset alias
- a list of supported external identifiers
Secrets
The client model is:
from mainsequence.client import Secret
A secret is protected configuration. The platform should treat it differently from normal application settings.
The current SDK model exposes:
uidnamevalueorganization_environment_uidandorganization_environment_name, the Environment that owns it
value is filled only when the secret is read with get() or get_by_uid();
listings and the answers to creating or changing a secret never carry it.
What secrets are good for
Use a secret for:
POLYGON_API_KEYOPENAI_API_KEYAWS_SECRET_ACCESS_KEY- broker credentials
- database passwords
- signed webhook tokens
If you would be uncomfortable seeing the value in a screenshot, error email, or notebook output, it should be a secret.
Secret examples
Create a secret:
from mainsequence.client import Secret
Secret.create(
name="POLYGON_API_KEY",
value="***",
)
Read a secret:
secret = Secret.get(name="POLYGON_API_KEY")
polygon_key = secret.value.get_secret_value() if secret.value is not None else None
Secret.get(name=...) first resolves the unique Environment-owned metadata row,
then retrieves its value through the UID detail endpoint. Secret.filter(...)
always remains metadata-only and never returns secret values. Secret.value is a
Pydantic SecretStr; unwrap it only where plaintext is required and never log it.
A validation error raised while reading a Secret does not include its input, so
it does not show the value either. Reading the value needs the secret's
value_reader policy or more; see Who can use a secret.
Filter secrets by name:
secrets = Secret.filter(name__in=["POLYGON_API_KEY", "OPENAI_API_KEY"])
Current client-side supported filters are:
namename__in
For idempotent secret setup, prefer resolving by name before create:
existing = {s.name for s in Secret.filter(name__in=["POLYGON_API_KEY"])}
if "POLYGON_API_KEY" not in existing:
Secret.create(name="POLYGON_API_KEY", value="***")
Who can use a secret
A secret's access is decided by its policies:
reader: see the secret, its name and its versions' dates, never its value;value_reader: also read its current value;editor: also read earlier values, set a new value, delete it, and see who has access;owner: also decide who has access.
The value needs value_reader or more, and earlier values editor or more.
Secret.get() and Secret.get_by_uid() read the current value; without
value_reader they raise PermissionDeniedError, whose payload names the
required_action, secrets.read_value, and the granting_policies.
Secret.filter() lists the secrets you can see without their values, so a
reader finds a secret by name there. Give a workload that uses a secret
value_reader; reader is for people who configure workloads that use the
secret but must never know it.
The person or workload that creates a secret holds owner, and Organization
admins hold every action. The policies go to people, workload identities and
Teams. A secret the platform manages, such as a hosted database password, cannot
get a new value or be deleted, by anyone: those requests answer 409. A
repository workflow file's access entry for a secret gives its workload the
policy it names, such as policy: value_reader for the current value.
Read and change the policies with the access methods on Secret:
from mainsequence.client import Secret
secret = Secret.filter(name="POLYGON_API_KEY")[0]
for binding in secret.get_access().bindings:
print(binding.uid, binding.policy.id, binding.principal.type, binding.principal.uid)
binding = secret.attach_policy(
"value_reader", principal_type="user", principal_uid=workload_user_uid
)
secret.detach_policy(binding.uid)
answer = secret.explain_access("secrets.read_value", principal_uid=user_uid)
print(answer.allowed, answer.granting_policies)
The access methods address the secret by its UID alone, so they need no
Environment. A refused change raises PermissionDeniedError too.
mainsequence access policies --type secrets lists the policies with their
actions, and Who Has Access to an Object describes every
method and model.
Operational rules for secrets
Secrets need a different operational standard than constants:
- do not commit them to the repository
- do not copy them into constants
- do not print them in logs
- do not paste them into application UIs or notebooks casually
- rotate them when external systems require it
A good working rule is:
- constants may be read frequently and discussed openly
- secrets should be accessed only where they are required
Human-Readable Examples
Example 1: Vendor data integration
You are integrating a vendor feed.
Use constants for:
- the vendor dataset name
- a default universe identifier
- a feature flag enabling a fallback path
Use secrets for:
- the vendor API key
- the vendor bearer token
Example split:
VENDOR__DATASET = "us_equities_realtime"as a constantVENDOR__DEFAULT_EXCHANGE = "XNYS"as a constantVENDOR_API_KEYas a secret
Example 2: Model configuration
You have a job that computes signals using a rolling window and a threshold.
Use constants for:
- rolling window
- minimum signal score
- default region
Do not use secrets here unless an external protected service is involved.
Example:
MODEL__DEFAULT_WINDOW = 252MODEL__MIN_SCORE = 0.85MODEL__REGION = "US"
Example 3: External execution venue
You have a workflow that talks to a broker or exchange.
Use constants for:
- default account alias
- execution mode such as
paperorlive - venue configuration labels
Use secrets for:
- API secret
- refresh token
- signing private key
CLI Usage
The CLI exposes both resources directly.
Constants:
mainsequence constants list
mainsequence constants create MODEL__DEFAULT_WINDOW 252
mainsequence constants create BROKER__DEFAULTS '{"mode":"paper"}'
mainsequence constants delete <CONSTANT_UID>
Secrets:
mainsequence secrets list
mainsequence secrets create POLYGON_API_KEY your-secret-value
mainsequence secrets delete <SECRET_UID>
Important behavior:
- constants display the category derived from the prefix before
__ - secrets are shown by metadata only in CLI tables and delete previews
- delete commands require typed verification
CLI access commands for constants
These commands read and change a constant's policies:
mainsequence constants access list <CONSTANT_UID>
mainsequence constants access grant <CONSTANT_UID> reader --user <USER_UID>
mainsequence constants access grant <CONSTANT_UID> editor --team <TEAM_UID>
mainsequence constants access revoke <CONSTANT_UID> <BINDING_UID>
mainsequence constants access explain <CONSTANT_UID> constants.update
Interpretation:
<CONSTANT_UID>is the constant public UID<USER_UID>is the public UUID of the person or workload identity receiving the policy, and<TEAM_UID>the team's<BINDING_UID>is the UID of a binding, whichaccess listshows
Unlike the other constants commands, these need no registered branch: the constant is addressed by its UID alone. See Constants in the CLI reference.
CLI access commands for secrets
These commands read and change a secret's policies:
mainsequence secrets access list <SECRET_UID>
mainsequence secrets access grant <SECRET_UID> value_reader --user <USER_UID>
mainsequence secrets access grant <SECRET_UID> editor --team <TEAM_UID>
mainsequence secrets access revoke <SECRET_UID> <BINDING_UID>
mainsequence secrets access explain <SECRET_UID> secrets.read_value
<SECRET_UID> is the secret public UID; <USER_UID>, <TEAM_UID> and
<BINDING_UID> are as for constants. Like the constant access commands, these
need no registered branch. secrets delete reads the secret, value included,
before it asks for confirmation, and then deletes it, so it needs editor or
owner. A refused command prints the platform's message and the policies that
would allow it. See Secrets in the CLI reference.
This is the most concrete CLI example of Main Sequence RBAC at the resource level.
Recommended Practice
A good configuration layout usually looks like this:
- constants contain readable runtime settings
- secrets contain credentials only
- application code reads both, but does not redefine them
A healthy split is:
- store business and runtime configuration in constants
- store credentials and protected tokens in secrets
That gives you:
- better readability
- safer operations
- fewer credentials hardcoded in repository code
Anti-Patterns
Avoid these:
- storing API keys in constants
- storing large application config blobs in secrets
- hardcoding credentials in source files
- using secrets for ordinary non-sensitive flags
- creating many nearly identical constants when one small JSON object would do
Final Rule
If the value is safe to read, review, and discuss as part of ordinary repository configuration, use a Constant.
If the value must be protected, use a Secret.