diff --git a/ProcessMaker/Http/Controllers/Api/EnvironmentVariablesController.php b/ProcessMaker/Http/Controllers/Api/EnvironmentVariablesController.php
index 8a127f3a5c..ffd892d65d 100644
--- a/ProcessMaker/Http/Controllers/Api/EnvironmentVariablesController.php
+++ b/ProcessMaker/Http/Controllers/Api/EnvironmentVariablesController.php
@@ -182,7 +182,7 @@ public function update(EnvironmentVariable $environment_variable, Request $reque
validator($data, EnvironmentVariable::rules($environment_variable), EnvironmentVariable::messages())->validate();
$linkedAsset = EnvironmentVariable::validateAssetLinkConsistency($data);
- $fields = ['name', 'description', 'asset_type'];
+ $fields = ['name', 'description', 'asset_type', 'do_not_update'];
if ($linkedAsset) {
// Guarantee value is always the selected asset's ID on this instance.
$data['value'] = (string) $linkedAsset->id;
diff --git a/ProcessMaker/ImportExport/Exporters/EnvironmentVariableExporter.php b/ProcessMaker/ImportExport/Exporters/EnvironmentVariableExporter.php
index 2a54c70c3a..ed1637ad45 100644
--- a/ProcessMaker/ImportExport/Exporters/EnvironmentVariableExporter.php
+++ b/ProcessMaker/ImportExport/Exporters/EnvironmentVariableExporter.php
@@ -2,9 +2,13 @@
namespace ProcessMaker\ImportExport\Exporters;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ExporterMap;
use ProcessMaker\ImportExport\DependentType;
+use ProcessMaker\ImportExport\Manifest;
+use ProcessMaker\ImportExport\Options;
+use ProcessMaker\ImportExport\Psudomodels\Psudomodel;
class EnvironmentVariableExporter extends ExporterBase
{
@@ -16,6 +20,18 @@ class EnvironmentVariableExporter extends ExporterBase
public $incrementStringSeparator = '_';
+ public function __construct(
+ Model|Psudomodel|null $model,
+ Manifest $manifest,
+ Options $options,
+ $ignoreExplicitDiscard
+ ) {
+ parent::__construct($model, $manifest, $options, $ignoreExplicitDiscard);
+
+ // PHP does not allow closures as property defaults.
+ $this->discard = fn ($envVar) => (bool) ($envVar->do_not_update ?? false);
+ }
+
public function export() : void
{
$this->addReference(DependentType::ENVIRONMENT_VARIABLE_VALUE, $this->model->value);
diff --git a/ProcessMaker/ImportExport/Exporters/ExporterBase.php b/ProcessMaker/ImportExport/Exporters/ExporterBase.php
index f4ac9c4d90..e951581bee 100644
--- a/ProcessMaker/ImportExport/Exporters/ExporterBase.php
+++ b/ProcessMaker/ImportExport/Exporters/ExporterBase.php
@@ -47,6 +47,12 @@ abstract class ExporterBase implements ExporterInterface
public $hidden = false;
+ /**
+ * When true (or a callable returning true), this asset is explicitly discarded.
+ * Callables receive the model: fn (Model $model): bool
+ *
+ * @var bool|callable
+ */
public $discard = false;
public static $forceUpdate = false;
@@ -135,7 +141,7 @@ public function uuid() : string
public function include() : bool
{
- if ($this->discard) { // Explicit Discard (not from passed-in options)
+ if ($this->isExplicitlyDiscarded()) { // Explicit Discard (not from passed-in options)
if (!$this->ignoreExplicitDiscard) {
return false;
}
@@ -148,6 +154,40 @@ public function include() : bool
return true;
}
+ /**
+ * Resolve $discard whether it is a boolean or a callable checked against the model.
+ */
+ public function isExplicitlyDiscarded(): bool
+ {
+ if (is_callable($this->discard)) {
+ return (bool) ($this->discard)($this->model);
+ }
+
+ return (bool) $this->discard;
+ }
+
+ /**
+ * Import-time check: should an existing target model be discarded (not overwritten)?
+ *
+ * Only callables are evaluated here. Boolean `$discard = true` (Users/Groups) is an
+ * export-time flag handled by include(), not an import-time skip.
+ * Psudomodels (e.g. Signal) are never discarded by this path.
+ */
+ public static function shouldDiscardExistingModelDuringImport(Model|Psudomodel|null $model): bool
+ {
+ if (!$model instanceof Model || !$model->exists) {
+ return false;
+ }
+
+ $exporter = new static($model, new Manifest(), new Options([]), false);
+
+ if (!is_callable($exporter->discard)) {
+ return false;
+ }
+
+ return (bool) ($exporter->discard)($model);
+ }
+
public function addDependent(string $type, Model|Psudomodel $dependentModel, string $exporterClass, $meta = null)
{
$uuid = $dependentModel->uuid;
@@ -304,7 +344,7 @@ public function toArray()
'hidden' => $this->hidden,
'mode' => $this->mode,
'saveAssetsMode' => $this->saveAssetsMode,
- 'explicit_discard' => $this->discard,
+ 'explicit_discard' => $this->isExplicitlyDiscarded(),
'dependents' => array_map(fn ($d) => $d->toArray(), $this->dependents),
'name' => $this->getName($this->model),
'description' => $this->getDescription(),
diff --git a/ProcessMaker/ImportExport/Manifest.php b/ProcessMaker/ImportExport/Manifest.php
index 494d3197b5..ac5372b777 100644
--- a/ProcessMaker/ImportExport/Manifest.php
+++ b/ProcessMaker/ImportExport/Manifest.php
@@ -158,6 +158,11 @@ public static function getModel($uuid, $assetInfo, $mode, $exporterClass, $isRoo
$mode = 'discard';
}
+ // Per-asset explicit discard (e.g. env vars with do_not_update on the target).
+ if ($model && $exporterClass::shouldDiscardExistingModelDuringImport($model)) {
+ $mode = 'discard';
+ }
+
if ($exporterClass::$forceUpdate) {
$mode = 'update';
}
diff --git a/ProcessMaker/Models/EnvironmentVariable.php b/ProcessMaker/Models/EnvironmentVariable.php
index 38f6e9342b..2cee3a1597 100644
--- a/ProcessMaker/Models/EnvironmentVariable.php
+++ b/ProcessMaker/Models/EnvironmentVariable.php
@@ -17,6 +17,7 @@
* @OA\Property(property="description", type="string"),
* @OA\Property(property="value", type="string"),
* @OA\Property(property="asset_type", type="string", nullable=true),
+ * @OA\Property(property="do_not_update", type="boolean"),
* ),
* @OA\Schema(
* schema="EnvironmentVariable",
@@ -41,6 +42,11 @@ class EnvironmentVariable extends ProcessMakerModel
'description',
'value',
'asset_type',
+ 'do_not_update',
+ ];
+
+ protected $casts = [
+ 'do_not_update' => 'boolean',
];
protected $hidden = [
@@ -102,6 +108,7 @@ public static function rules($existing = null)
'string',
Rule::in($allowedAssetTypes),
],
+ 'do_not_update' => 'boolean',
];
}
diff --git a/database/factories/ProcessMaker/Models/EnvironmentVariableFactory.php b/database/factories/ProcessMaker/Models/EnvironmentVariableFactory.php
index 9c63ccdf2f..fbdf253ce9 100644
--- a/database/factories/ProcessMaker/Models/EnvironmentVariableFactory.php
+++ b/database/factories/ProcessMaker/Models/EnvironmentVariableFactory.php
@@ -18,6 +18,7 @@ public function definition()
'name' => $this->faker->word(),
'description' => $this->faker->sentence(),
'value' => $this->faker->sentence(),
+ 'do_not_update' => false,
];
}
}
diff --git a/database/migrations/2026_07_23_000000_add_do_not_update_to_environment_variables_table.php b/database/migrations/2026_07_23_000000_add_do_not_update_to_environment_variables_table.php
new file mode 100644
index 0000000000..122397d593
--- /dev/null
+++ b/database/migrations/2026_07_23_000000_add_do_not_update_to_environment_variables_table.php
@@ -0,0 +1,28 @@
+boolean('do_not_update')->default(false)->after('asset_type');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('do_not_update');
+ });
+ }
+};
diff --git a/resources/js/processes/environment-variables/components/CreateEnvironmentVariableModal.vue b/resources/js/processes/environment-variables/components/CreateEnvironmentVariableModal.vue
index 77b52b5a73..ca604e149e 100644
--- a/resources/js/processes/environment-variables/components/CreateEnvironmentVariableModal.vue
+++ b/resources/js/processes/environment-variables/components/CreateEnvironmentVariableModal.vue
@@ -41,6 +41,7 @@
:value.sync="value"
:errors="errors"
/>
+