Variables

Envless.variables.apply

Sync an environment in one transaction

def apply(
  project,
  environment,
  changes = nil,
  product: nil,
  timeout: nil,
  **fields
)

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 Hash 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

ruby
require "envless/sdk"

passphrase = ENV.fetch("ENVLESS_PASSPHRASE")
workspace_id = Envless.me.get[:workspaceId]
current = Envless.variables.list_all("api", "production")

baseline = current.map do |variable|
  {
    uniqueId: variable[:id],
    name: variable[:name],
    updatedAt: variable[:updatedAt] || variable[:createdAt]
  }
end
legacy = current.select do |variable|
  variable[:name].start_with?("LEGACY_")
end
flag = Envless.encrypt_value("true", passphrase, workspace_id)

result = Envless.variables.apply(
  "api",
  "production",
  baseline:,
  creates: [{name: "FEATURE_FLAG", value: flag}],
  deletes: legacy.map { |variable| {uniqueId: variable[:id]} }
)

puts "#{result[:added]} #{result[:updated]} #{result[:removed]}"

Parameters

ParameterTypeRequiredDescription
projectStringYesProject slug the environment belongs to.
environmentStringYesEnvironment slug to sync.
changesapply_changesNoThe fields below as one Hash, with Symbol or String keys, in place of the keywords.
baseline:Array[apply_baseline_entry]YesEvery live variable as you last read it, up to 1000 entries.
creates:Array[variable_input]NoNew variables, where only name and value are written. name matches ^[A-Za-z_][A-Za-z0-9_]*$, 1 to 100 characters.
updates:Array[apply_update_entry]NoValue replacements addressed by uniqueId. Add name to rename the variable in the same transaction; it is validated like a create name.
deletes:Array[{ uniqueId: String }]NoSoft deletes addressed by uniqueId.
product:String | SymbolNoProduct slug, needed when the project slug is not unique in the workspace.
timeout:Integer | Float | RationalNoSeconds each attempt of this call may take, redirects and the response body included, before it raises Envless::NetworkError with timeout? set. Overrides the client's timeout for this call, and 0 turns the limit off.

Returns

apply_result 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 failed 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 Envless::ApiError 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.