This was significantly easier than expected. Here's an example of what an extension class might look like:
<?php final class AddRiskReviewHeraldCustomAction extends HeraldCustomAction { public function appliesToAdapter(HeraldAdapter $adapter) { return $adapter instanceof HeraldDifferentialRevisionAdapter; } public function appliesToRuleType($rule_type) { return $rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL || $rule_type == HeraldRuleTypeConfig::RULE_TYPE_OBJECT; } public function getActionKey() { return 'custom:add-risk'; } public function getActionName() { return 'Add risk rating (JSON)'; } public function getActionType() { return HeraldAdapter::VALUE_TEXT; } public function applyEffect( HeraldAdapter $adapter, $object, HeraldEffect $effect) { $key = "phragile:risk-rating"; // Read existing value. $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_VIEW); $field_list->readFieldsFromStorage($object); $field_list = mpull($field_list->getFields(), null, 'getFieldKey'); $field = $field_list[$key]; $field->setObject($object); $field->setViewer(PhabricatorUser::getOmnipotentUser()); $risk = $field->getValue(); $old_risk = $risk; // PHP copies arrays by default! // Add new value to array. $herald_args = phutil_json_decode($effect->getTarget()); $risk[$herald_args['key']] = array( 'value' => $herald_args['value'], 'reason' => $herald_args['reason']); $risk_key = $herald_args['key']; // Set new value. $adapter->queueTransaction( id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) ->setMetadataValue('customfield:key', $key) ->setOldValue($old_risk) ->setNewValue($risk)); return new HeraldApplyTranscript( $effect, true, pht( 'Modifying automatic risk ratings (key: %s)!', $risk_key)); } }