Variables

envless.variables.apply()

Sync an environment in one transaction

def apply(
    project: str,
    environment: str,
    changes: ApplyChanges,
    *,
    product: str | None = None,
    timeout: float | None = None,
) -> ApplyResult

Runs creates, updates and deletes against an environment inside one transaction, behind a per environment advisory lock. This is the method a sync client should use instead of a loop of single calls: the environment is never left half written and two concurrent applies cannot interleave.

baseline is required and is how drift is detected. Send every live variable as you last read it, as a dict of uniqueId, name and updatedAt filled from the id, name and updatedAt of a read. Before touching anything the server compares your baseline against the live rows and refuses the whole request with 409 variable.baseline_drifted when a live variable is missing from your baseline, a baseline entry no longer exists, or an updatedAt differs by a single millisecond. A partial baseline is not a partial check, it is guaranteed drift. The drifted names appear only in the error message, so on a 409 re-read the environment and rebuild the change set.

Apply is value only and name only. creates write name and value and take the column defaults for everything else, updates write value alone, and there is no rename and no defaultValue path here, use update or update_by_id for those. Every creates and updates value goes through the same v1:/v2: prefix check create enforces, and one plaintext value refuses the whole change set with 400 variable.ciphertext_invalid before anything is written.

Every creates name must match ^[A-Za-z_][A-Za-z0-9_]*$ and be 1 to 100 characters, and is stored with exactly the casing you send. Names are compared without regard to case, so two creates entries differing only in case are a repeat, and creating api_key while API_KEY is live and not in deletes is a conflict. baseline names are not pattern checked, send each one back exactly as you read it so a legacy name still round-trips.

Example

import os

from envless import encrypt_value, envless

passphrase = os.environ['ENVLESS_PASSPHRASE']
workspace_id = envless.me.get()['workspaceId']
current = envless.variables.list_all('api', 'production')

result = envless.variables.apply(
    'api',
    'production',
    {
        'baseline': [
            {
                'uniqueId': variable['id'],
                'name': variable['name'],
                'updatedAt': variable['updatedAt']
                or variable['createdAt'],
            }
            for variable in current
        ],
        'creates': [
            {
                'name': 'FEATURE_FLAG',
                'value': encrypt_value(
                    'true', passphrase, workspace_id
                ),
            }
        ],
        'deletes': [
            {'uniqueId': variable['id']}
            for variable in current
            if variable['name'].startswith('LEGACY_')
        ],
    },
)

print(result['added'], result['updated'], result['removed'])

Parameters

ParameterTypeRequiredDescription
projectstrYesProject slug the environment belongs to.
environmentstrYesEnvironment slug to sync.
changes['baseline']list[ApplyBaselineEntry]YesEvery live variable as you last read it, up to 1000 entries.
changes['creates']list[VariableInput]NoNew variables, where only name and value are written. name matches ^[A-Za-z_][A-Za-z0-9_]*$, 1 to 100 characters.
changes['updates']list[ApplyUpdateEntry]NoValue replacements addressed by uniqueId. Add name to rename the variable in the same transaction; it is validated like a create name.
changes['deletes']list[ApplyChangesDeletesItem]NoSoft deletes addressed by uniqueId.
productstrNoProduct slug, needed when the project slug is not unique in the workspace.
timeoutfloatNoSeconds each attempt of this call may take, the response body included, before it raises EnvlessNetworkError with is_timeout set. Overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ApplyResult with added, updated and removed counts for what the transaction wrote.

Worth knowing

  • At most 1000 baseline entries, and at most 1000 items across creates, updates and deletes combined. At least one of the three must be non-empty.
  • 409 variable.baseline_drifted is reused for a creates name already taken and for an updates or deletes id that is no longer live, so read message to tell the three apart.
  • A rejected apply writes nothing at all, so re-reading and retrying is always safe.
  • Names are case reserved, not case normalised. A name freed by a deletes entry in the same request can be reused by a creates entry, including under a different casing.
  • Every creates name is checked locally before anything is sent, so an unusable name raises EnvlessApiError carrying request.body_invalid without touching the transaction.

Required scopes

variables:write

Underlying endpoint

POST /projects/{slug}/environments/{environmentSlug}/variables/apply

Request and response schema, every error code, and a live playground.

In TypeScript

envless.variables.apply()

The same method in @goenvless/sdk, with the same request, retries and errors.