diff --git a/README.md b/README.md
index e31b521..0060b60 100644
--- a/README.md
+++ b/README.md
@@ -259,6 +259,63 @@ activitysmith.live_activities.end(
)
```
+### Live Activity Action
+
+Just like Actionable Push Notifications, Live Activities can have a button that opens provided URL in a browser or triggers a webhook. Webhooks are executed by the ActivitySmith backend.
+
+
+
+
+
+#### Open URL action
+
+```ruby
+start = activitysmith.live_activities.start(
+ {
+ content_state: {
+ title: "Deploying payments-api",
+ subtitle: "Running database migrations",
+ number_of_steps: 5,
+ current_step: 3,
+ type: "segmented_progress"
+ },
+ action: {
+ title: "Open Workflow",
+ type: "open_url",
+ url: "https://github.com/acme/payments-api/actions/runs/1234567890"
+ }
+ }
+)
+
+activity_id = start.activity_id
+```
+
+#### Webhook action
+
+```ruby
+activitysmith.live_activities.update(
+ {
+ activity_id: activity_id,
+ content_state: {
+ title: "Reindexing product search",
+ subtitle: "Shard 7 of 12",
+ number_of_steps: 12,
+ current_step: 7
+ },
+ action: {
+ title: "Pause Reindex",
+ type: "webhook",
+ url: "https://ops.example.com/hooks/search/reindex/pause",
+ method: "POST",
+ body: {
+ job_id: "reindex-2026-03-19",
+ requested_by: "activitysmith-ruby"
+ }
+ }
+ }
+)
+```
+
## Channels
Channels are used to target specific team members or devices. Can be used for both push notifications and live activities.
diff --git a/generated/activitysmith_openapi.rb b/generated/activitysmith_openapi.rb
index a4ea80c..79f7d55 100644
--- a/generated/activitysmith_openapi.rb
+++ b/generated/activitysmith_openapi.rb
@@ -24,6 +24,8 @@
require 'activitysmith_openapi/models/content_state_start'
require 'activitysmith_openapi/models/content_state_update'
require 'activitysmith_openapi/models/forbidden_error'
+require 'activitysmith_openapi/models/live_activity_action'
+require 'activitysmith_openapi/models/live_activity_action_type'
require 'activitysmith_openapi/models/live_activity_end_request'
require 'activitysmith_openapi/models/live_activity_end_response'
require 'activitysmith_openapi/models/live_activity_limit_error'
@@ -31,6 +33,7 @@
require 'activitysmith_openapi/models/live_activity_start_response'
require 'activitysmith_openapi/models/live_activity_update_request'
require 'activitysmith_openapi/models/live_activity_update_response'
+require 'activitysmith_openapi/models/live_activity_webhook_method'
require 'activitysmith_openapi/models/no_recipients_error'
require 'activitysmith_openapi/models/push_notification_action'
require 'activitysmith_openapi/models/push_notification_action_type'
diff --git a/generated/activitysmith_openapi/models/live_activity_action.rb b/generated/activitysmith_openapi/models/live_activity_action.rb
new file mode 100644
index 0000000..d54fbb5
--- /dev/null
+++ b/generated/activitysmith_openapi/models/live_activity_action.rb
@@ -0,0 +1,323 @@
+=begin
+#ActivitySmith API
+
+#Send push notifications and Live Activities to your own devices via a single API key.
+
+The version of the OpenAPI document: 1.0.0
+
+Generated by: https://openapi-generator.tech
+Generator version: 7.7.0
+
+=end
+
+require 'date'
+require 'time'
+
+module OpenapiClient
+ # Optional single action button shown in the Live Activity UI.
+ class LiveActivityAction
+ # Button title displayed in the Live Activity UI.
+ attr_accessor :title
+
+ attr_accessor :type
+
+ # HTTPS URL. For open_url it is opened in browser. For webhook it is called by ActivitySmith backend.
+ attr_accessor :url
+
+ # Webhook HTTP method. Used only when type=webhook.
+ attr_accessor :method
+
+ # Optional webhook payload body. Used only when type=webhook.
+ attr_accessor :body
+
+ class EnumAttributeValidator
+ attr_reader :datatype
+ attr_reader :allowable_values
+
+ def initialize(datatype, allowable_values)
+ @allowable_values = allowable_values.map do |value|
+ case datatype.to_s
+ when /Integer/i
+ value.to_i
+ when /Float/i
+ value.to_f
+ else
+ value
+ end
+ end
+ end
+
+ def valid?(value)
+ !value || allowable_values.include?(value)
+ end
+ end
+
+ # Attribute mapping from ruby-style variable name to JSON key.
+ def self.attribute_map
+ {
+ :'title' => :'title',
+ :'type' => :'type',
+ :'url' => :'url',
+ :'method' => :'method',
+ :'body' => :'body'
+ }
+ end
+
+ # Returns all the JSON keys this model knows about
+ def self.acceptable_attributes
+ attribute_map.values
+ end
+
+ # Attribute type mapping.
+ def self.openapi_types
+ {
+ :'title' => :'String',
+ :'type' => :'LiveActivityActionType',
+ :'url' => :'String',
+ :'method' => :'LiveActivityWebhookMethod',
+ :'body' => :'Hash'
+ }
+ end
+
+ # List of attributes with nullable: true
+ def self.openapi_nullable
+ Set.new([
+ ])
+ end
+
+ # Initializes the object
+ # @param [Hash] attributes Model attributes in the form of hash
+ def initialize(attributes = {})
+ if (!attributes.is_a?(Hash))
+ fail ArgumentError, "The input argument (attributes) must be a hash in `OpenapiClient::LiveActivityAction` initialize method"
+ end
+
+ # check to see if the attribute exists and convert string to symbol for hash key
+ attributes = attributes.each_with_object({}) { |(k, v), h|
+ if (!self.class.attribute_map.key?(k.to_sym))
+ fail ArgumentError, "`#{k}` is not a valid attribute in `OpenapiClient::LiveActivityAction`. Please check the name to make sure it's valid. List of attributes: " + self.class.attribute_map.keys.inspect
+ end
+ h[k.to_sym] = v
+ }
+
+ if attributes.key?(:'title')
+ self.title = attributes[:'title']
+ else
+ self.title = nil
+ end
+
+ if attributes.key?(:'type')
+ self.type = attributes[:'type']
+ else
+ self.type = nil
+ end
+
+ if attributes.key?(:'url')
+ self.url = attributes[:'url']
+ else
+ self.url = nil
+ end
+
+ if attributes.key?(:'method')
+ self.method = attributes[:'method']
+ else
+ self.method = 'POST'
+ end
+
+ if attributes.key?(:'body')
+ if (value = attributes[:'body']).is_a?(Hash)
+ self.body = value
+ end
+ end
+ end
+
+ # Show invalid properties with the reasons. Usually used together with valid?
+ # @return Array for valid properties with the reasons
+ def list_invalid_properties
+ warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
+ invalid_properties = Array.new
+ if @title.nil?
+ invalid_properties.push('invalid value for "title", title cannot be nil.')
+ end
+
+ if @type.nil?
+ invalid_properties.push('invalid value for "type", type cannot be nil.')
+ end
+
+ if @url.nil?
+ invalid_properties.push('invalid value for "url", url cannot be nil.')
+ end
+
+ pattern = Regexp.new(/^https:\/\//)
+ if @url !~ pattern
+ invalid_properties.push("invalid value for \"url\", must conform to the pattern #{pattern}.")
+ end
+
+ invalid_properties
+ end
+
+ # Check to see if the all the properties in the model are valid
+ # @return true if the model is valid
+ def valid?
+ warn '[DEPRECATED] the `valid?` method is obsolete'
+ return false if @title.nil?
+ return false if @type.nil?
+ return false if @url.nil?
+ return false if @url !~ Regexp.new(/^https:\/\//)
+ true
+ end
+
+ # Custom attribute writer method with validation
+ # @param [Object] url Value to be assigned
+ def url=(url)
+ if url.nil?
+ fail ArgumentError, 'url cannot be nil'
+ end
+
+ pattern = Regexp.new(/^https:\/\//)
+ if url !~ pattern
+ fail ArgumentError, "invalid value for \"url\", must conform to the pattern #{pattern}."
+ end
+
+ @url = url
+ end
+
+ # Checks equality by comparing each attribute.
+ # @param [Object] Object to be compared
+ def ==(o)
+ return true if self.equal?(o)
+ self.class == o.class &&
+ title == o.title &&
+ type == o.type &&
+ url == o.url &&
+ method == o.method &&
+ body == o.body
+ end
+
+ # @see the `==` method
+ # @param [Object] Object to be compared
+ def eql?(o)
+ self == o
+ end
+
+ # Calculates hash code according to all attributes.
+ # @return [Integer] Hash code
+ def hash
+ [title, type, url, method, body].hash
+ end
+
+ # Builds the object from hash
+ # @param [Hash] attributes Model attributes in the form of hash
+ # @return [Object] Returns the model itself
+ def self.build_from_hash(attributes)
+ return nil unless attributes.is_a?(Hash)
+ attributes = attributes.transform_keys(&:to_sym)
+ transformed_hash = {}
+ openapi_types.each_pair do |key, type|
+ if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil?
+ transformed_hash["#{key}"] = nil
+ elsif type =~ /\AArray<(.*)>/i
+ # check to ensure the input is an array given that the attribute
+ # is documented as an array but the input is not
+ if attributes[attribute_map[key]].is_a?(Array)
+ transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) }
+ end
+ elsif !attributes[attribute_map[key]].nil?
+ transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]])
+ end
+ end
+ new(transformed_hash)
+ end
+
+ # Deserializes the data based on type
+ # @param string type Data type
+ # @param string value Value to be deserialized
+ # @return [Object] Deserialized data
+ def self._deserialize(type, value)
+ case type.to_sym
+ when :Time
+ Time.parse(value)
+ when :Date
+ Date.parse(value)
+ when :String
+ value.to_s
+ when :Integer
+ value.to_i
+ when :Float
+ value.to_f
+ when :Boolean
+ if value.to_s =~ /\A(true|t|yes|y|1)\z/i
+ true
+ else
+ false
+ end
+ when :Object
+ # generic object (usually a Hash), return directly
+ value
+ when /\AArray<(?.+)>\z/
+ inner_type = Regexp.last_match[:inner_type]
+ value.map { |v| _deserialize(inner_type, v) }
+ when /\AHash<(?.+?), (?.+)>\z/
+ k_type = Regexp.last_match[:k_type]
+ v_type = Regexp.last_match[:v_type]
+ {}.tap do |hash|
+ value.each do |k, v|
+ hash[_deserialize(k_type, k)] = _deserialize(v_type, v)
+ end
+ end
+ else # model
+ # models (e.g. Pet) or oneOf
+ klass = OpenapiClient.const_get(type)
+ klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value)
+ end
+ end
+
+ # Returns the string representation of the object
+ # @return [String] String presentation of the object
+ def to_s
+ to_hash.to_s
+ end
+
+ # to_body is an alias to to_hash (backward compatibility)
+ # @return [Hash] Returns the object in the form of hash
+ def to_body
+ to_hash
+ end
+
+ # Returns the object in the form of hash
+ # @return [Hash] Returns the object in the form of hash
+ def to_hash
+ hash = {}
+ self.class.attribute_map.each_pair do |attr, param|
+ value = self.send(attr)
+ if value.nil?
+ is_nullable = self.class.openapi_nullable.include?(attr)
+ next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}"))
+ end
+
+ hash[param] = _to_hash(value)
+ end
+ hash
+ end
+
+ # Outputs non-array value in the form of hash
+ # For object, use to_hash. Otherwise, just return the value
+ # @param [Object] value Any valid value
+ # @return [Hash] Returns the value in the form of hash
+ def _to_hash(value)
+ if value.is_a?(Array)
+ value.compact.map { |v| _to_hash(v) }
+ elsif value.is_a?(Hash)
+ {}.tap do |hash|
+ value.each { |k, v| hash[k] = _to_hash(v) }
+ end
+ elsif value.respond_to? :to_hash
+ value.to_hash
+ else
+ value
+ end
+ end
+
+ end
+
+end
diff --git a/generated/activitysmith_openapi/models/live_activity_action_type.rb b/generated/activitysmith_openapi/models/live_activity_action_type.rb
new file mode 100644
index 0000000..0510ff3
--- /dev/null
+++ b/generated/activitysmith_openapi/models/live_activity_action_type.rb
@@ -0,0 +1,40 @@
+=begin
+#ActivitySmith API
+
+#Send push notifications and Live Activities to your own devices via a single API key.
+
+The version of the OpenAPI document: 1.0.0
+
+Generated by: https://openapi-generator.tech
+Generator version: 7.7.0
+
+=end
+
+require 'date'
+require 'time'
+
+module OpenapiClient
+ class LiveActivityActionType
+ OPEN_URL = "open_url".freeze
+ WEBHOOK = "webhook".freeze
+
+ def self.all_vars
+ @all_vars ||= [OPEN_URL, WEBHOOK].freeze
+ end
+
+ # Builds the enum from string
+ # @param [String] The enum value in the form of the string
+ # @return [String] The enum value
+ def self.build_from_hash(value)
+ new.build_from_hash(value)
+ end
+
+ # Builds the enum from string
+ # @param [String] The enum value in the form of the string
+ # @return [String] The enum value
+ def build_from_hash(value)
+ return value if LiveActivityActionType.all_vars.include?(value)
+ raise "Invalid ENUM value #{value} for class #LiveActivityActionType"
+ end
+ end
+end
diff --git a/generated/activitysmith_openapi/models/live_activity_end_request.rb b/generated/activitysmith_openapi/models/live_activity_end_request.rb
index de9cf25..7e79cd9 100644
--- a/generated/activitysmith_openapi/models/live_activity_end_request.rb
+++ b/generated/activitysmith_openapi/models/live_activity_end_request.rb
@@ -20,11 +20,14 @@ class LiveActivityEndRequest
attr_accessor :content_state
+ attr_accessor :action
+
# Attribute mapping from ruby-style variable name to JSON key.
def self.attribute_map
{
:'activity_id' => :'activity_id',
- :'content_state' => :'content_state'
+ :'content_state' => :'content_state',
+ :'action' => :'action'
}
end
@@ -37,7 +40,8 @@ def self.acceptable_attributes
def self.openapi_types
{
:'activity_id' => :'String',
- :'content_state' => :'ContentStateEnd'
+ :'content_state' => :'ContentStateEnd',
+ :'action' => :'LiveActivityAction'
}
end
@@ -73,6 +77,10 @@ def initialize(attributes = {})
else
self.content_state = nil
end
+
+ if attributes.key?(:'action')
+ self.action = attributes[:'action']
+ end
end
# Show invalid properties with the reasons. Usually used together with valid?
@@ -106,7 +114,8 @@ def ==(o)
return true if self.equal?(o)
self.class == o.class &&
activity_id == o.activity_id &&
- content_state == o.content_state
+ content_state == o.content_state &&
+ action == o.action
end
# @see the `==` method
@@ -118,7 +127,7 @@ def eql?(o)
# Calculates hash code according to all attributes.
# @return [Integer] Hash code
def hash
- [activity_id, content_state].hash
+ [activity_id, content_state, action].hash
end
# Builds the object from hash
diff --git a/generated/activitysmith_openapi/models/live_activity_start_request.rb b/generated/activitysmith_openapi/models/live_activity_start_request.rb
index c315d29..afeba0a 100644
--- a/generated/activitysmith_openapi/models/live_activity_start_request.rb
+++ b/generated/activitysmith_openapi/models/live_activity_start_request.rb
@@ -18,6 +18,8 @@ module OpenapiClient
class LiveActivityStartRequest
attr_accessor :content_state
+ attr_accessor :action
+
attr_accessor :alert
attr_accessor :target
@@ -26,6 +28,7 @@ class LiveActivityStartRequest
def self.attribute_map
{
:'content_state' => :'content_state',
+ :'action' => :'action',
:'alert' => :'alert',
:'target' => :'target'
}
@@ -40,6 +43,7 @@ def self.acceptable_attributes
def self.openapi_types
{
:'content_state' => :'ContentStateStart',
+ :'action' => :'LiveActivityAction',
:'alert' => :'AlertPayload',
:'target' => :'ChannelTarget'
}
@@ -72,6 +76,10 @@ def initialize(attributes = {})
self.content_state = nil
end
+ if attributes.key?(:'action')
+ self.action = attributes[:'action']
+ end
+
if attributes.key?(:'alert')
self.alert = attributes[:'alert']
end
@@ -107,6 +115,7 @@ def ==(o)
return true if self.equal?(o)
self.class == o.class &&
content_state == o.content_state &&
+ action == o.action &&
alert == o.alert &&
target == o.target
end
@@ -120,7 +129,7 @@ def eql?(o)
# Calculates hash code according to all attributes.
# @return [Integer] Hash code
def hash
- [content_state, alert, target].hash
+ [content_state, action, alert, target].hash
end
# Builds the object from hash
diff --git a/generated/activitysmith_openapi/models/live_activity_update_request.rb b/generated/activitysmith_openapi/models/live_activity_update_request.rb
index d42ee37..4b9aa14 100644
--- a/generated/activitysmith_openapi/models/live_activity_update_request.rb
+++ b/generated/activitysmith_openapi/models/live_activity_update_request.rb
@@ -20,11 +20,14 @@ class LiveActivityUpdateRequest
attr_accessor :content_state
+ attr_accessor :action
+
# Attribute mapping from ruby-style variable name to JSON key.
def self.attribute_map
{
:'activity_id' => :'activity_id',
- :'content_state' => :'content_state'
+ :'content_state' => :'content_state',
+ :'action' => :'action'
}
end
@@ -37,7 +40,8 @@ def self.acceptable_attributes
def self.openapi_types
{
:'activity_id' => :'String',
- :'content_state' => :'ContentStateUpdate'
+ :'content_state' => :'ContentStateUpdate',
+ :'action' => :'LiveActivityAction'
}
end
@@ -73,6 +77,10 @@ def initialize(attributes = {})
else
self.content_state = nil
end
+
+ if attributes.key?(:'action')
+ self.action = attributes[:'action']
+ end
end
# Show invalid properties with the reasons. Usually used together with valid?
@@ -106,7 +114,8 @@ def ==(o)
return true if self.equal?(o)
self.class == o.class &&
activity_id == o.activity_id &&
- content_state == o.content_state
+ content_state == o.content_state &&
+ action == o.action
end
# @see the `==` method
@@ -118,7 +127,7 @@ def eql?(o)
# Calculates hash code according to all attributes.
# @return [Integer] Hash code
def hash
- [activity_id, content_state].hash
+ [activity_id, content_state, action].hash
end
# Builds the object from hash
diff --git a/generated/activitysmith_openapi/models/live_activity_webhook_method.rb b/generated/activitysmith_openapi/models/live_activity_webhook_method.rb
new file mode 100644
index 0000000..2c10830
--- /dev/null
+++ b/generated/activitysmith_openapi/models/live_activity_webhook_method.rb
@@ -0,0 +1,40 @@
+=begin
+#ActivitySmith API
+
+#Send push notifications and Live Activities to your own devices via a single API key.
+
+The version of the OpenAPI document: 1.0.0
+
+Generated by: https://openapi-generator.tech
+Generator version: 7.7.0
+
+=end
+
+require 'date'
+require 'time'
+
+module OpenapiClient
+ class LiveActivityWebhookMethod
+ GET = "GET".freeze
+ POST = "POST".freeze
+
+ def self.all_vars
+ @all_vars ||= [GET, POST].freeze
+ end
+
+ # Builds the enum from string
+ # @param [String] The enum value in the form of the string
+ # @return [String] The enum value
+ def self.build_from_hash(value)
+ new.build_from_hash(value)
+ end
+
+ # Builds the enum from string
+ # @param [String] The enum value in the form of the string
+ # @return [String] The enum value
+ def build_from_hash(value)
+ return value if LiveActivityWebhookMethod.all_vars.include?(value)
+ raise "Invalid ENUM value #{value} for class #LiveActivityWebhookMethod"
+ end
+ end
+end
diff --git a/generated/activitysmith_openapi/version.rb b/generated/activitysmith_openapi/version.rb
index 9d17b21..3aaa6f4 100644
--- a/generated/activitysmith_openapi/version.rb
+++ b/generated/activitysmith_openapi/version.rb
@@ -11,5 +11,5 @@
=end
module OpenapiClient
- VERSION = '0.1.6'
+ VERSION = '1.0.0'
end
diff --git a/lib/activitysmith/version.rb b/lib/activitysmith/version.rb
index 8836827..eba5f20 100644
--- a/lib/activitysmith/version.rb
+++ b/lib/activitysmith/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module ActivitySmith
- VERSION = "0.1.7"
+ VERSION = "1.0.0"
end
diff --git a/test/resources_test.rb b/test/resources_test.rb
index bcfce38..ac0b4df 100644
--- a/test/resources_test.rb
+++ b/test/resources_test.rb
@@ -222,6 +222,73 @@ def test_live_activities_support_progress_payloads
)
end
+ def test_live_activities_pass_action_payloads_through
+ api = FakeLiveApi.new
+ resource = ActivitySmith::LiveActivities.new(api)
+
+ start_payload = {
+ content_state: {
+ title: "Deploying payments-api",
+ subtitle: "Running database migrations",
+ number_of_steps: 5,
+ current_step: 3,
+ type: "segmented_progress"
+ },
+ action: {
+ title: "Open Workflow",
+ type: "open_url",
+ url: "https://github.com/acme/payments-api/actions/runs/1234567890"
+ }
+ }
+
+ update_payload = {
+ activity_id: "act-1",
+ content_state: {
+ title: "Reindexing product search",
+ subtitle: "Shard 7 of 12",
+ number_of_steps: 12,
+ current_step: 7
+ },
+ action: {
+ title: "Pause Reindex",
+ type: "webhook",
+ url: "https://ops.example.com/hooks/search/reindex/pause",
+ method: "POST",
+ body: {
+ job_id: "reindex-2026-03-19"
+ }
+ }
+ }
+
+ end_payload = {
+ activity_id: "act-1",
+ content_state: {
+ title: "Deploying payments-api",
+ subtitle: "Production rollout complete",
+ number_of_steps: 5,
+ current_step: 5
+ },
+ action: {
+ title: "Open Workflow",
+ type: "open_url",
+ url: "https://github.com/acme/payments-api/actions/runs/1234567890"
+ }
+ }
+
+ resource.start(start_payload)
+ resource.update(update_payload)
+ resource.end(end_payload)
+
+ assert_equal(
+ [
+ [:start_live_activity, start_payload, {}],
+ [:update_live_activity, update_payload, {}],
+ [:end_live_activity, end_payload, {}]
+ ],
+ api.calls
+ )
+ end
+
def test_passthrough_methods
push_api = FakePushApi.new
notifications = ActivitySmith::Notifications.new(push_api)