def apply(
project: str,
environment: str,
changes: ApplyChanges,
*,
product: str | None = None,
timeout: float | None = None,
) -> ApplyResultasync def apply(
project: str,
environment: str,
changes: ApplyChanges,
*,
product: str | None = None,
timeout: float | None = None,
) -> ApplyResultRuns 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'])import asyncio
import os
from envless import AsyncEnvless, encrypt_value
async def main() -> None:
async with AsyncEnvless() as envless:
passphrase = os.environ['ENVLESS_PASSPHRASE']
workspace_id = (await envless.me.get())['workspaceId']
current = await envless.variables.list_all('api', 'production')
result = await 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'])
asyncio.run(main())Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
project | str | Yes | Project slug the environment belongs to. |
environment | str | Yes | Environment slug to sync. |
changes['baseline'] | list[ApplyBaselineEntry] | Yes | Every live variable as you last read it, up to 1000 entries. |
changes['creates'] | list[VariableInput] | No | New 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] | No | Value 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] | No | Soft deletes addressed by uniqueId. |
product | str | No | Product slug, needed when the project slug is not unique in the workspace. |
timeout | float | No | Seconds 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,updatesanddeletescombined. At least one of the three must be non-empty. - 409
variable.baseline_driftedis reused for acreatesname already taken and for anupdatesordeletesid that is no longer live, so readmessageto 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
deletesentry in the same request can be reused by acreatesentry, including under a different casing. - Every
createsname is checked locally before anything is sent, so an unusable name raisesEnvlessApiErrorcarryingrequest.body_invalidwithout 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.