diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba5eb0c7507a..4e659bf81352 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
 
 ## [Unreleased]
 
+### Features
+
+* (x/group) [#16191](https://github.com/cosmos/cosmos-sdk/pull/16191) Add EventProposalPruned event to group module whenever a proposal is pruned.
+
 ### Improvements
 
 * (deps) [#16083](https://github.com/cosmos/cosmos-sdk/pull/16083) Bumps `proto-builder` image to 0.13.0.
diff --git a/api/cosmos/group/v1/events.pulsar.go b/api/cosmos/group/v1/events.pulsar.go
index 5352013f75c7..deebbe181739 100644
--- a/api/cosmos/group/v1/events.pulsar.go
+++ b/api/cosmos/group/v1/events.pulsar.go
@@ -3857,6 +3857,537 @@ func (x *fastReflection_EventLeaveGroup) ProtoMethods() *protoiface.Methods {
 	}
 }
 
+var (
+	md_EventProposalPruned              protoreflect.MessageDescriptor
+	fd_EventProposalPruned_proposal_id  protoreflect.FieldDescriptor
+	fd_EventProposalPruned_status       protoreflect.FieldDescriptor
+	fd_EventProposalPruned_tally_result protoreflect.FieldDescriptor
+)
+
+func init() {
+	file_cosmos_group_v1_events_proto_init()
+	md_EventProposalPruned = File_cosmos_group_v1_events_proto.Messages().ByName("EventProposalPruned")
+	fd_EventProposalPruned_proposal_id = md_EventProposalPruned.Fields().ByName("proposal_id")
+	fd_EventProposalPruned_status = md_EventProposalPruned.Fields().ByName("status")
+	fd_EventProposalPruned_tally_result = md_EventProposalPruned.Fields().ByName("tally_result")
+}
+
+var _ protoreflect.Message = (*fastReflection_EventProposalPruned)(nil)
+
+type fastReflection_EventProposalPruned EventProposalPruned
+
+func (x *EventProposalPruned) ProtoReflect() protoreflect.Message {
+	return (*fastReflection_EventProposalPruned)(x)
+}
+
+func (x *EventProposalPruned) slowProtoReflect() protoreflect.Message {
+	mi := &file_cosmos_group_v1_events_proto_msgTypes[9]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+var _fastReflection_EventProposalPruned_messageType fastReflection_EventProposalPruned_messageType
+var _ protoreflect.MessageType = fastReflection_EventProposalPruned_messageType{}
+
+type fastReflection_EventProposalPruned_messageType struct{}
+
+func (x fastReflection_EventProposalPruned_messageType) Zero() protoreflect.Message {
+	return (*fastReflection_EventProposalPruned)(nil)
+}
+func (x fastReflection_EventProposalPruned_messageType) New() protoreflect.Message {
+	return new(fastReflection_EventProposalPruned)
+}
+func (x fastReflection_EventProposalPruned_messageType) Descriptor() protoreflect.MessageDescriptor {
+	return md_EventProposalPruned
+}
+
+// Descriptor returns message descriptor, which contains only the protobuf
+// type information for the message.
+func (x *fastReflection_EventProposalPruned) Descriptor() protoreflect.MessageDescriptor {
+	return md_EventProposalPruned
+}
+
+// Type returns the message type, which encapsulates both Go and protobuf
+// type information. If the Go type information is not needed,
+// it is recommended that the message descriptor be used instead.
+func (x *fastReflection_EventProposalPruned) Type() protoreflect.MessageType {
+	return _fastReflection_EventProposalPruned_messageType
+}
+
+// New returns a newly allocated and mutable empty message.
+func (x *fastReflection_EventProposalPruned) New() protoreflect.Message {
+	return new(fastReflection_EventProposalPruned)
+}
+
+// Interface unwraps the message reflection interface and
+// returns the underlying ProtoMessage interface.
+func (x *fastReflection_EventProposalPruned) Interface() protoreflect.ProtoMessage {
+	return (*EventProposalPruned)(x)
+}
+
+// Range iterates over every populated field in an undefined order,
+// calling f for each field descriptor and value encountered.
+// Range returns immediately if f returns false.
+// While iterating, mutating operations may only be performed
+// on the current field descriptor.
+func (x *fastReflection_EventProposalPruned) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) {
+	if x.ProposalId != uint64(0) {
+		value := protoreflect.ValueOfUint64(x.ProposalId)
+		if !f(fd_EventProposalPruned_proposal_id, value) {
+			return
+		}
+	}
+	if x.Status != 0 {
+		value := protoreflect.ValueOfEnum((protoreflect.EnumNumber)(x.Status))
+		if !f(fd_EventProposalPruned_status, value) {
+			return
+		}
+	}
+	if x.TallyResult != nil {
+		value := protoreflect.ValueOfMessage(x.TallyResult.ProtoReflect())
+		if !f(fd_EventProposalPruned_tally_result, value) {
+			return
+		}
+	}
+}
+
+// Has reports whether a field is populated.
+//
+// Some fields have the property of nullability where it is possible to
+// distinguish between the default value of a field and whether the field
+// was explicitly populated with the default value. Singular message fields,
+// member fields of a oneof, and proto2 scalar fields are nullable. Such
+// fields are populated only if explicitly set.
+//
+// In other cases (aside from the nullable cases above),
+// a proto3 scalar field is populated if it contains a non-zero value, and
+// a repeated field is populated if it is non-empty.
+func (x *fastReflection_EventProposalPruned) Has(fd protoreflect.FieldDescriptor) bool {
+	switch fd.FullName() {
+	case "cosmos.group.v1.EventProposalPruned.proposal_id":
+		return x.ProposalId != uint64(0)
+	case "cosmos.group.v1.EventProposalPruned.status":
+		return x.Status != 0
+	case "cosmos.group.v1.EventProposalPruned.tally_result":
+		return x.TallyResult != nil
+	default:
+		if fd.IsExtension() {
+			panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned"))
+		}
+		panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName()))
+	}
+}
+
+// Clear clears the field such that a subsequent Has call reports false.
+//
+// Clearing an extension field clears both the extension type and value
+// associated with the given field number.
+//
+// Clear is a mutating operation and unsafe for concurrent use.
+func (x *fastReflection_EventProposalPruned) Clear(fd protoreflect.FieldDescriptor) {
+	switch fd.FullName() {
+	case "cosmos.group.v1.EventProposalPruned.proposal_id":
+		x.ProposalId = uint64(0)
+	case "cosmos.group.v1.EventProposalPruned.status":
+		x.Status = 0
+	case "cosmos.group.v1.EventProposalPruned.tally_result":
+		x.TallyResult = nil
+	default:
+		if fd.IsExtension() {
+			panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned"))
+		}
+		panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName()))
+	}
+}
+
+// Get retrieves the value for a field.
+//
+// For unpopulated scalars, it returns the default value, where
+// the default value of a bytes scalar is guaranteed to be a copy.
+// For unpopulated composite types, it returns an empty, read-only view
+// of the value; to obtain a mutable reference, use Mutable.
+func (x *fastReflection_EventProposalPruned) Get(descriptor protoreflect.FieldDescriptor) protoreflect.Value {
+	switch descriptor.FullName() {
+	case "cosmos.group.v1.EventProposalPruned.proposal_id":
+		value := x.ProposalId
+		return protoreflect.ValueOfUint64(value)
+	case "cosmos.group.v1.EventProposalPruned.status":
+		value := x.Status
+		return protoreflect.ValueOfEnum((protoreflect.EnumNumber)(value))
+	case "cosmos.group.v1.EventProposalPruned.tally_result":
+		value := x.TallyResult
+		return protoreflect.ValueOfMessage(value.ProtoReflect())
+	default:
+		if descriptor.IsExtension() {
+			panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned"))
+		}
+		panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", descriptor.FullName()))
+	}
+}
+
+// Set stores the value for a field.
+//
+// For a field belonging to a oneof, it implicitly clears any other field
+// that may be currently set within the same oneof.
+// For extension fields, it implicitly stores the provided ExtensionType.
+// When setting a composite type, it is unspecified whether the stored value
+// aliases the source's memory in any way. If the composite value is an
+// empty, read-only value, then it panics.
+//
+// Set is a mutating operation and unsafe for concurrent use.
+func (x *fastReflection_EventProposalPruned) Set(fd protoreflect.FieldDescriptor, value protoreflect.Value) {
+	switch fd.FullName() {
+	case "cosmos.group.v1.EventProposalPruned.proposal_id":
+		x.ProposalId = value.Uint()
+	case "cosmos.group.v1.EventProposalPruned.status":
+		x.Status = (ProposalStatus)(value.Enum())
+	case "cosmos.group.v1.EventProposalPruned.tally_result":
+		x.TallyResult = value.Message().Interface().(*TallyResult)
+	default:
+		if fd.IsExtension() {
+			panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned"))
+		}
+		panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName()))
+	}
+}
+
+// Mutable returns a mutable reference to a composite type.
+//
+// If the field is unpopulated, it may allocate a composite value.
+// For a field belonging to a oneof, it implicitly clears any other field
+// that may be currently set within the same oneof.
+// For extension fields, it implicitly stores the provided ExtensionType
+// if not already stored.
+// It panics if the field does not contain a composite type.
+//
+// Mutable is a mutating operation and unsafe for concurrent use.
+func (x *fastReflection_EventProposalPruned) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value {
+	switch fd.FullName() {
+	case "cosmos.group.v1.EventProposalPruned.tally_result":
+		if x.TallyResult == nil {
+			x.TallyResult = new(TallyResult)
+		}
+		return protoreflect.ValueOfMessage(x.TallyResult.ProtoReflect())
+	case "cosmos.group.v1.EventProposalPruned.proposal_id":
+		panic(fmt.Errorf("field proposal_id of message cosmos.group.v1.EventProposalPruned is not mutable"))
+	case "cosmos.group.v1.EventProposalPruned.status":
+		panic(fmt.Errorf("field status of message cosmos.group.v1.EventProposalPruned is not mutable"))
+	default:
+		if fd.IsExtension() {
+			panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned"))
+		}
+		panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName()))
+	}
+}
+
+// NewField returns a new value that is assignable to the field
+// for the given descriptor. For scalars, this returns the default value.
+// For lists, maps, and messages, this returns a new, empty, mutable value.
+func (x *fastReflection_EventProposalPruned) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value {
+	switch fd.FullName() {
+	case "cosmos.group.v1.EventProposalPruned.proposal_id":
+		return protoreflect.ValueOfUint64(uint64(0))
+	case "cosmos.group.v1.EventProposalPruned.status":
+		return protoreflect.ValueOfEnum(0)
+	case "cosmos.group.v1.EventProposalPruned.tally_result":
+		m := new(TallyResult)
+		return protoreflect.ValueOfMessage(m.ProtoReflect())
+	default:
+		if fd.IsExtension() {
+			panic(fmt.Errorf("proto3 declared messages do not support extensions: cosmos.group.v1.EventProposalPruned"))
+		}
+		panic(fmt.Errorf("message cosmos.group.v1.EventProposalPruned does not contain field %s", fd.FullName()))
+	}
+}
+
+// WhichOneof reports which field within the oneof is populated,
+// returning nil if none are populated.
+// It panics if the oneof descriptor does not belong to this message.
+func (x *fastReflection_EventProposalPruned) WhichOneof(d protoreflect.OneofDescriptor) protoreflect.FieldDescriptor {
+	switch d.FullName() {
+	default:
+		panic(fmt.Errorf("%s is not a oneof field in cosmos.group.v1.EventProposalPruned", d.FullName()))
+	}
+	panic("unreachable")
+}
+
+// GetUnknown retrieves the entire list of unknown fields.
+// The caller may only mutate the contents of the RawFields
+// if the mutated bytes are stored back into the message with SetUnknown.
+func (x *fastReflection_EventProposalPruned) GetUnknown() protoreflect.RawFields {
+	return x.unknownFields
+}
+
+// SetUnknown stores an entire list of unknown fields.
+// The raw fields must be syntactically valid according to the wire format.
+// An implementation may panic if this is not the case.
+// Once stored, the caller must not mutate the content of the RawFields.
+// An empty RawFields may be passed to clear the fields.
+//
+// SetUnknown is a mutating operation and unsafe for concurrent use.
+func (x *fastReflection_EventProposalPruned) SetUnknown(fields protoreflect.RawFields) {
+	x.unknownFields = fields
+}
+
+// IsValid reports whether the message is valid.
+//
+// An invalid message is an empty, read-only value.
+//
+// An invalid message often corresponds to a nil pointer of the concrete
+// message type, but the details are implementation dependent.
+// Validity is not part of the protobuf data model, and may not
+// be preserved in marshaling or other operations.
+func (x *fastReflection_EventProposalPruned) IsValid() bool {
+	return x != nil
+}
+
+// ProtoMethods returns optional fastReflectionFeature-path implementations of various operations.
+// This method may return nil.
+//
+// The returned methods type is identical to
+// "google.golang.org/protobuf/runtime/protoiface".Methods.
+// Consult the protoiface package documentation for details.
+func (x *fastReflection_EventProposalPruned) ProtoMethods() *protoiface.Methods {
+	size := func(input protoiface.SizeInput) protoiface.SizeOutput {
+		x := input.Message.Interface().(*EventProposalPruned)
+		if x == nil {
+			return protoiface.SizeOutput{
+				NoUnkeyedLiterals: input.NoUnkeyedLiterals,
+				Size:              0,
+			}
+		}
+		options := runtime.SizeInputToOptions(input)
+		_ = options
+		var n int
+		var l int
+		_ = l
+		if x.ProposalId != 0 {
+			n += 1 + runtime.Sov(uint64(x.ProposalId))
+		}
+		if x.Status != 0 {
+			n += 1 + runtime.Sov(uint64(x.Status))
+		}
+		if x.TallyResult != nil {
+			l = options.Size(x.TallyResult)
+			n += 1 + l + runtime.Sov(uint64(l))
+		}
+		if x.unknownFields != nil {
+			n += len(x.unknownFields)
+		}
+		return protoiface.SizeOutput{
+			NoUnkeyedLiterals: input.NoUnkeyedLiterals,
+			Size:              n,
+		}
+	}
+
+	marshal := func(input protoiface.MarshalInput) (protoiface.MarshalOutput, error) {
+		x := input.Message.Interface().(*EventProposalPruned)
+		if x == nil {
+			return protoiface.MarshalOutput{
+				NoUnkeyedLiterals: input.NoUnkeyedLiterals,
+				Buf:               input.Buf,
+			}, nil
+		}
+		options := runtime.MarshalInputToOptions(input)
+		_ = options
+		size := options.Size(x)
+		dAtA := make([]byte, size)
+		i := len(dAtA)
+		_ = i
+		var l int
+		_ = l
+		if x.unknownFields != nil {
+			i -= len(x.unknownFields)
+			copy(dAtA[i:], x.unknownFields)
+		}
+		if x.TallyResult != nil {
+			encoded, err := options.Marshal(x.TallyResult)
+			if err != nil {
+				return protoiface.MarshalOutput{
+					NoUnkeyedLiterals: input.NoUnkeyedLiterals,
+					Buf:               input.Buf,
+				}, err
+			}
+			i -= len(encoded)
+			copy(dAtA[i:], encoded)
+			i = runtime.EncodeVarint(dAtA, i, uint64(len(encoded)))
+			i--
+			dAtA[i] = 0x1a
+		}
+		if x.Status != 0 {
+			i = runtime.EncodeVarint(dAtA, i, uint64(x.Status))
+			i--
+			dAtA[i] = 0x10
+		}
+		if x.ProposalId != 0 {
+			i = runtime.EncodeVarint(dAtA, i, uint64(x.ProposalId))
+			i--
+			dAtA[i] = 0x8
+		}
+		if input.Buf != nil {
+			input.Buf = append(input.Buf, dAtA...)
+		} else {
+			input.Buf = dAtA
+		}
+		return protoiface.MarshalOutput{
+			NoUnkeyedLiterals: input.NoUnkeyedLiterals,
+			Buf:               input.Buf,
+		}, nil
+	}
+	unmarshal := func(input protoiface.UnmarshalInput) (protoiface.UnmarshalOutput, error) {
+		x := input.Message.Interface().(*EventProposalPruned)
+		if x == nil {
+			return protoiface.UnmarshalOutput{
+				NoUnkeyedLiterals: input.NoUnkeyedLiterals,
+				Flags:             input.Flags,
+			}, nil
+		}
+		options := runtime.UnmarshalInputToOptions(input)
+		_ = options
+		dAtA := input.Buf
+		l := len(dAtA)
+		iNdEx := 0
+		for iNdEx < l {
+			preIndex := iNdEx
+			var wire uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				wire |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			fieldNum := int32(wire >> 3)
+			wireType := int(wire & 0x7)
+			if wireType == 4 {
+				return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: EventProposalPruned: wiretype end group for non-group")
+			}
+			if fieldNum <= 0 {
+				return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: EventProposalPruned: illegal tag %d (wire type %d)", fieldNum, wire)
+			}
+			switch fieldNum {
+			case 1:
+				if wireType != 0 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field ProposalId", wireType)
+				}
+				x.ProposalId = 0
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow
+					}
+					if iNdEx >= l {
+						return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					x.ProposalId |= uint64(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+			case 2:
+				if wireType != 0 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field Status", wireType)
+				}
+				x.Status = 0
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow
+					}
+					if iNdEx >= l {
+						return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					x.Status |= ProposalStatus(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+			case 3:
+				if wireType != 2 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, fmt.Errorf("proto: wrong wireType = %d for field TallyResult", wireType)
+				}
+				var msglen int
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrIntOverflow
+					}
+					if iNdEx >= l {
+						return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+					}
+					b := dAtA[iNdEx]
+					iNdEx++
+					msglen |= int(b&0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				if msglen < 0 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength
+				}
+				postIndex := iNdEx + msglen
+				if postIndex < 0 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength
+				}
+				if postIndex > l {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+				}
+				if x.TallyResult == nil {
+					x.TallyResult = &TallyResult{}
+				}
+				if err := options.Unmarshal(dAtA[iNdEx:postIndex], x.TallyResult); err != nil {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err
+				}
+				iNdEx = postIndex
+			default:
+				iNdEx = preIndex
+				skippy, err := runtime.Skip(dAtA[iNdEx:])
+				if err != nil {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, err
+				}
+				if (skippy < 0) || (iNdEx+skippy) < 0 {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, runtime.ErrInvalidLength
+				}
+				if (iNdEx + skippy) > l {
+					return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+				}
+				if !options.DiscardUnknown {
+					x.unknownFields = append(x.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+				}
+				iNdEx += skippy
+			}
+		}
+
+		if iNdEx > l {
+			return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, io.ErrUnexpectedEOF
+		}
+		return protoiface.UnmarshalOutput{NoUnkeyedLiterals: input.NoUnkeyedLiterals, Flags: input.Flags}, nil
+	}
+	return &protoiface.Methods{
+		NoUnkeyedLiterals: struct{}{},
+		Flags:             protoiface.SupportMarshalDeterministic | protoiface.SupportUnmarshalDiscardUnknown,
+		Size:              size,
+		Marshal:           marshal,
+		Unmarshal:         unmarshal,
+		Merge:             nil,
+		CheckInitialized:  nil,
+	}
+}
+
 // Since: cosmos-sdk 0.46
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
@@ -4232,6 +4763,61 @@ func (x *EventLeaveGroup) GetAddress() string {
 	return ""
 }
 
+// EventProposalPruned is an event emitted when a proposal is pruned.
+type EventProposalPruned struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// proposal_id is the unique ID of the proposal.
+	ProposalId uint64 `protobuf:"varint,1,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty"`
+	// status is the proposal status (UNSPECIFIED, SUBMITTED, ACCEPTED, REJECTED, ABORTED, WITHDRAWN).
+	Status ProposalStatus `protobuf:"varint,2,opt,name=status,proto3,enum=cosmos.group.v1.ProposalStatus" json:"status,omitempty"`
+	// tally_result is the proposal tally result (when applicable).
+	TallyResult *TallyResult `protobuf:"bytes,3,opt,name=tally_result,json=tallyResult,proto3" json:"tally_result,omitempty"`
+}
+
+func (x *EventProposalPruned) Reset() {
+	*x = EventProposalPruned{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_cosmos_group_v1_events_proto_msgTypes[9]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *EventProposalPruned) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*EventProposalPruned) ProtoMessage() {}
+
+// Deprecated: Use EventProposalPruned.ProtoReflect.Descriptor instead.
+func (*EventProposalPruned) Descriptor() ([]byte, []int) {
+	return file_cosmos_group_v1_events_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *EventProposalPruned) GetProposalId() uint64 {
+	if x != nil {
+		return x.ProposalId
+	}
+	return 0
+}
+
+func (x *EventProposalPruned) GetStatus() ProposalStatus {
+	if x != nil {
+		return x.Status
+	}
+	return ProposalStatus_PROPOSAL_STATUS_UNSPECIFIED
+}
+
+func (x *EventProposalPruned) GetTallyResult() *TallyResult {
+	if x != nil {
+		return x.TallyResult
+	}
+	return nil
+}
+
 var File_cosmos_group_v1_events_proto protoreflect.FileDescriptor
 
 var file_cosmos_group_v1_events_proto_rawDesc = []byte{
@@ -4281,18 +4867,29 @@ var file_cosmos_group_v1_events_proto_rawDesc = []byte{
 	0x70, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02,
 	0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73,
 	0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07,
-	0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0xaa, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e,
-	0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x76, 0x31, 0x42,
-	0x0b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28,
-	0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69,
-	0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2f, 0x76, 0x31,
-	0x3b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x47, 0x58, 0xaa, 0x02,
-	0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x56, 0x31,
-	0xca, 0x02, 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x5c,
-	0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x72, 0x6f, 0x75,
-	0x70, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
-	0xea, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x47, 0x72, 0x6f, 0x75, 0x70,
-	0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xb0, 0x01, 0x0a, 0x13, 0x45, 0x76, 0x65, 0x6e,
+	0x74, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x50, 0x72, 0x75, 0x6e, 0x65, 0x64, 0x12,
+	0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x49, 0x64,
+	0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
+	0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e,
+	0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3f, 0x0a, 0x0c, 0x74, 0x61, 0x6c,
+	0x6c, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1c, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e, 0x76,
+	0x31, 0x2e, 0x54, 0x61, 0x6c, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x0b, 0x74,
+	0x61, 0x6c, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0xaa, 0x01, 0x0a, 0x13, 0x63,
+	0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x2e,
+	0x76, 0x31, 0x42, 0x0b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
+	0x01, 0x5a, 0x28, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x73, 0x64, 0x6b, 0x2e, 0x69, 0x6f, 0x2f,
+	0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x67, 0x72, 0x6f, 0x75, 0x70,
+	0x2f, 0x76, 0x31, 0x3b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x47,
+	0x58, 0xaa, 0x02, 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70,
+	0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47, 0x72, 0x6f,
+	0x75, 0x70, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5c, 0x47,
+	0x72, 0x6f, 0x75, 0x70, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
+	0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x47, 0x72,
+	0x6f, 0x75, 0x70, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -4307,7 +4904,7 @@ func file_cosmos_group_v1_events_proto_rawDescGZIP() []byte {
 	return file_cosmos_group_v1_events_proto_rawDescData
 }
 
-var file_cosmos_group_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
+var file_cosmos_group_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
 var file_cosmos_group_v1_events_proto_goTypes = []interface{}{
 	(*EventCreateGroup)(nil),       // 0: cosmos.group.v1.EventCreateGroup
 	(*EventUpdateGroup)(nil),       // 1: cosmos.group.v1.EventUpdateGroup
@@ -4318,15 +4915,20 @@ var file_cosmos_group_v1_events_proto_goTypes = []interface{}{
 	(*EventVote)(nil),              // 6: cosmos.group.v1.EventVote
 	(*EventExec)(nil),              // 7: cosmos.group.v1.EventExec
 	(*EventLeaveGroup)(nil),        // 8: cosmos.group.v1.EventLeaveGroup
-	(ProposalExecutorResult)(0),    // 9: cosmos.group.v1.ProposalExecutorResult
+	(*EventProposalPruned)(nil),    // 9: cosmos.group.v1.EventProposalPruned
+	(ProposalExecutorResult)(0),    // 10: cosmos.group.v1.ProposalExecutorResult
+	(ProposalStatus)(0),            // 11: cosmos.group.v1.ProposalStatus
+	(*TallyResult)(nil),            // 12: cosmos.group.v1.TallyResult
 }
 var file_cosmos_group_v1_events_proto_depIdxs = []int32{
-	9, // 0: cosmos.group.v1.EventExec.result:type_name -> cosmos.group.v1.ProposalExecutorResult
-	1, // [1:1] is the sub-list for method output_type
-	1, // [1:1] is the sub-list for method input_type
-	1, // [1:1] is the sub-list for extension type_name
-	1, // [1:1] is the sub-list for extension extendee
-	0, // [0:1] is the sub-list for field type_name
+	10, // 0: cosmos.group.v1.EventExec.result:type_name -> cosmos.group.v1.ProposalExecutorResult
+	11, // 1: cosmos.group.v1.EventProposalPruned.status:type_name -> cosmos.group.v1.ProposalStatus
+	12, // 2: cosmos.group.v1.EventProposalPruned.tally_result:type_name -> cosmos.group.v1.TallyResult
+	3,  // [3:3] is the sub-list for method output_type
+	3,  // [3:3] is the sub-list for method input_type
+	3,  // [3:3] is the sub-list for extension type_name
+	3,  // [3:3] is the sub-list for extension extendee
+	0,  // [0:3] is the sub-list for field type_name
 }
 
 func init() { file_cosmos_group_v1_events_proto_init() }
@@ -4444,6 +5046,18 @@ func file_cosmos_group_v1_events_proto_init() {
 				return nil
 			}
 		}
+		file_cosmos_group_v1_events_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*EventProposalPruned); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -4451,7 +5065,7 @@ func file_cosmos_group_v1_events_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_cosmos_group_v1_events_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   9,
+			NumMessages:   10,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/api/cosmos/group/v1/types.pulsar.go b/api/cosmos/group/v1/types.pulsar.go
index bc96cac970a4..8ccb79977323 100644
--- a/api/cosmos/group/v1/types.pulsar.go
+++ b/api/cosmos/group/v1/types.pulsar.go
@@ -7965,6 +7965,8 @@ type GroupPolicyInfo struct {
 	// admin is the account address of the group admin.
 	Admin string `protobuf:"bytes,3,opt,name=admin,proto3" json:"admin,omitempty"`
 	// metadata is any arbitrary metadata attached to the group policy.
+	// the recommended format of the metadata is to be found here:
+	// https://docs.cosmos.network/v0.47/modules/group#decision-policy-1
 	Metadata string `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
 	// version is used to track changes to a group's GroupPolicyInfo structure that
 	// would create a different result on a running proposal.
@@ -8058,6 +8060,8 @@ type Proposal struct {
 	// group_policy_address is the account address of group policy.
 	GroupPolicyAddress string `protobuf:"bytes,2,opt,name=group_policy_address,json=groupPolicyAddress,proto3" json:"group_policy_address,omitempty"`
 	// metadata is any arbitrary metadata attached to the proposal.
+	// the recommended format of the metadata is to be found here:
+	// https://docs.cosmos.network/v0.47/modules/group#proposal-4
 	Metadata string `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"`
 	// proposers are the account addresses of the proposers.
 	Proposers []string `protobuf:"bytes,4,rep,name=proposers,proto3" json:"proposers,omitempty"`
diff --git a/proto/cosmos/group/v1/events.proto b/proto/cosmos/group/v1/events.proto
index c2cfe8728f72..2b98ec9abc32 100644
--- a/proto/cosmos/group/v1/events.proto
+++ b/proto/cosmos/group/v1/events.proto
@@ -79,3 +79,16 @@ message EventLeaveGroup {
   // address is the account address of the group member.
   string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
 }
+
+// EventProposalPruned is an event emitted when a proposal is pruned.
+message EventProposalPruned {
+
+  // proposal_id is the unique ID of the proposal.
+  uint64 proposal_id = 1;
+
+  // status is the proposal status (UNSPECIFIED, SUBMITTED, ACCEPTED, REJECTED, ABORTED, WITHDRAWN).
+  ProposalStatus status = 2;
+
+  // tally_result is the proposal tally result (when applicable).
+  TallyResult tally_result = 3;
+}
diff --git a/proto/cosmos/group/v1/query.proto b/proto/cosmos/group/v1/query.proto
index f141d26b9352..80b09255afed 100644
--- a/proto/cosmos/group/v1/query.proto
+++ b/proto/cosmos/group/v1/query.proto
@@ -85,7 +85,7 @@ service Query {
   };
 
   // Groups queries all groups in state.
-  // 
+  //
   // Since: cosmos-sdk 0.47.1
   rpc Groups(QueryGroupsRequest) returns (QueryGroupsResponse) {
     option (google.api.http).get = "/cosmos/group/v1/groups";
@@ -300,7 +300,7 @@ message QueryTallyResultResponse {
 }
 
 // QueryGroupsRequest is the Query/Groups request type.
-// 
+//
 // Since: cosmos-sdk 0.47.1
 message QueryGroupsRequest {
 
@@ -309,7 +309,7 @@ message QueryGroupsRequest {
 }
 
 // QueryGroupsResponse is the Query/Groups response type.
-// 
+//
 // Since: cosmos-sdk 0.47.1
 message QueryGroupsResponse {
   // `groups` is all the groups present in state.
diff --git a/proto/cosmos/group/v1/types.proto b/proto/cosmos/group/v1/types.proto
index 99838401ead8..4968d13cb7c4 100644
--- a/proto/cosmos/group/v1/types.proto
+++ b/proto/cosmos/group/v1/types.proto
@@ -171,6 +171,8 @@ message GroupPolicyInfo {
   string admin = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];
 
   // metadata is any arbitrary metadata attached to the group policy.
+  // the recommended format of the metadata is to be found here:
+  // https://docs.cosmos.network/v0.47/modules/group#decision-policy-1
   string metadata = 4;
 
   // version is used to track changes to a group's GroupPolicyInfo structure that
@@ -199,6 +201,8 @@ message Proposal {
   string group_policy_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
 
   // metadata is any arbitrary metadata attached to the proposal.
+  // the recommended format of the metadata is to be found here:
+  // https://docs.cosmos.network/v0.47/modules/group#proposal-4
   string metadata = 3;
 
   // proposers are the account addresses of the proposers.
diff --git a/x/group/README.md b/x/group/README.md
index b94fec728650..29e5478b5fb2 100644
--- a/x/group/README.md
+++ b/x/group/README.md
@@ -49,6 +49,7 @@ This module allows the creation and management of on-chain multisig accounts and
     * [EventVote](#eventvote)
     * [EventExec](#eventexec)
     * [EventLeaveGroup](#eventleavegroup)
+    * [EventProposalPruned](#eventproposalpruned)
 * [Client](#client)
     * [CLI](#cli)
     * [gRPC](#grpc)
@@ -578,6 +579,15 @@ The group module emits the following events:
 | cosmos.group.v1.EventLeaveGroup | proposal_id   | {proposalId}                    |
 | cosmos.group.v1.EventLeaveGroup | address       | {address}                       |
 
+### EventProposalPruned
+
+| Type                                | Attribute Key | Attribute Value                 |
+|-------------------------------------|---------------|---------------------------------|
+| message                             | action        | /cosmos.group.v1.Msg/LeaveGroup |
+| cosmos.group.v1.EventProposalPruned | proposal_id   | {proposalId}                    |
+| cosmos.group.v1.EventProposalPruned | status        | {ProposalStatus}                |
+| cosmos.group.v1.EventProposalPruned | tally_result  | {TallyResult}                   |
+
 
 ## Client
 
diff --git a/x/group/events.pb.go b/x/group/events.pb.go
index 1e30b1f06c9d..dc869b695c66 100644
--- a/x/group/events.pb.go
+++ b/x/group/events.pb.go
@@ -464,6 +464,70 @@ func (m *EventLeaveGroup) GetAddress() string {
 	return ""
 }
 
+// EventProposalPruned is an event emitted when a proposal is pruned.
+type EventProposalPruned struct {
+	// proposal_id is the unique ID of the proposal.
+	ProposalId uint64 `protobuf:"varint,1,opt,name=proposal_id,json=proposalId,proto3" json:"proposal_id,omitempty"`
+	// status is the proposal status (UNSPECIFIED, SUBMITTED, ACCEPTED, REJECTED, ABORTED, WITHDRAWN).
+	Status ProposalStatus `protobuf:"varint,2,opt,name=status,proto3,enum=cosmos.group.v1.ProposalStatus" json:"status,omitempty"`
+	// tally_result is the proposal tally result (when applicable).
+	TallyResult *TallyResult `protobuf:"bytes,3,opt,name=tally_result,json=tallyResult,proto3" json:"tally_result,omitempty"`
+}
+
+func (m *EventProposalPruned) Reset()         { *m = EventProposalPruned{} }
+func (m *EventProposalPruned) String() string { return proto.CompactTextString(m) }
+func (*EventProposalPruned) ProtoMessage()    {}
+func (*EventProposalPruned) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e8d753981546f032, []int{9}
+}
+func (m *EventProposalPruned) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *EventProposalPruned) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_EventProposalPruned.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *EventProposalPruned) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_EventProposalPruned.Merge(m, src)
+}
+func (m *EventProposalPruned) XXX_Size() int {
+	return m.Size()
+}
+func (m *EventProposalPruned) XXX_DiscardUnknown() {
+	xxx_messageInfo_EventProposalPruned.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_EventProposalPruned proto.InternalMessageInfo
+
+func (m *EventProposalPruned) GetProposalId() uint64 {
+	if m != nil {
+		return m.ProposalId
+	}
+	return 0
+}
+
+func (m *EventProposalPruned) GetStatus() ProposalStatus {
+	if m != nil {
+		return m.Status
+	}
+	return PROPOSAL_STATUS_UNSPECIFIED
+}
+
+func (m *EventProposalPruned) GetTallyResult() *TallyResult {
+	if m != nil {
+		return m.TallyResult
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterType((*EventCreateGroup)(nil), "cosmos.group.v1.EventCreateGroup")
 	proto.RegisterType((*EventUpdateGroup)(nil), "cosmos.group.v1.EventUpdateGroup")
@@ -474,36 +538,41 @@ func init() {
 	proto.RegisterType((*EventVote)(nil), "cosmos.group.v1.EventVote")
 	proto.RegisterType((*EventExec)(nil), "cosmos.group.v1.EventExec")
 	proto.RegisterType((*EventLeaveGroup)(nil), "cosmos.group.v1.EventLeaveGroup")
+	proto.RegisterType((*EventProposalPruned)(nil), "cosmos.group.v1.EventProposalPruned")
 }
 
 func init() { proto.RegisterFile("cosmos/group/v1/events.proto", fileDescriptor_e8d753981546f032) }
 
 var fileDescriptor_e8d753981546f032 = []byte{
-	// 382 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x49, 0xce, 0x2f, 0xce,
-	0xcd, 0x2f, 0xd6, 0x4f, 0x2f, 0xca, 0x2f, 0x2d, 0xd0, 0x2f, 0x33, 0xd4, 0x4f, 0x2d, 0x4b, 0xcd,
-	0x2b, 0x29, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x87, 0xc8, 0xea, 0x81, 0x65, 0xf5,
-	0xca, 0x0c, 0xa5, 0x24, 0x21, 0x02, 0xf1, 0x60, 0x69, 0x7d, 0xa8, 0x2c, 0x98, 0x23, 0x25, 0x8d,
-	0x6e, 0x52, 0x49, 0x65, 0x41, 0x2a, 0x54, 0x52, 0x49, 0x97, 0x4b, 0xc0, 0x15, 0x64, 0xb0, 0x73,
-	0x51, 0x6a, 0x62, 0x49, 0xaa, 0x3b, 0x48, 0x89, 0x90, 0x24, 0x17, 0x07, 0x58, 0x6d, 0x7c, 0x66,
-	0x8a, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x4b, 0x10, 0x3b, 0x98, 0xef, 0x99, 0x02, 0x57, 0x1e, 0x5a,
-	0x90, 0x42, 0x8c, 0x72, 0x1f, 0x2e, 0x31, 0x74, 0xd3, 0x03, 0xf2, 0x73, 0x32, 0x93, 0x2b, 0x85,
-	0x8c, 0xb8, 0xd8, 0x13, 0x53, 0x52, 0x8a, 0x52, 0x8b, 0x8b, 0xc1, 0x7a, 0x38, 0x9d, 0x24, 0x2e,
-	0x6d, 0xd1, 0x15, 0x81, 0xba, 0xdb, 0x11, 0x22, 0x13, 0x5c, 0x52, 0x94, 0x99, 0x97, 0x1e, 0x04,
-	0x53, 0x08, 0x37, 0x0d, 0xc9, 0x72, 0x0a, 0x4c, 0x33, 0xe3, 0x12, 0x06, 0x9b, 0x16, 0x5c, 0x9a,
-	0x94, 0x9b, 0x59, 0x12, 0x50, 0x94, 0x5f, 0x90, 0x5f, 0x9c, 0x98, 0x23, 0x24, 0xcf, 0xc5, 0x5d,
-	0x00, 0x65, 0x23, 0x3c, 0xc4, 0x05, 0x13, 0xf2, 0x4c, 0x51, 0xb2, 0xe0, 0x12, 0x05, 0xeb, 0x0b,
-	0xcf, 0x2c, 0xc9, 0x48, 0x29, 0x4a, 0x2c, 0x27, 0x5e, 0xa7, 0x0e, 0x17, 0x27, 0x58, 0x67, 0x58,
-	0x7e, 0x49, 0x2a, 0x61, 0xd5, 0x8d, 0x8c, 0x50, 0xe5, 0xae, 0x15, 0xa9, 0xc9, 0x04, 0x95, 0x0b,
-	0xd9, 0x73, 0xb1, 0x15, 0xa5, 0x16, 0x97, 0xe6, 0x94, 0x48, 0x30, 0x29, 0x30, 0x6a, 0xf0, 0x19,
-	0xa9, 0xeb, 0xa1, 0x25, 0x11, 0x3d, 0x98, 0x43, 0x41, 0xe6, 0x95, 0x96, 0xe4, 0x17, 0x05, 0x81,
-	0x95, 0x07, 0x41, 0xb5, 0x09, 0x09, 0x71, 0xb1, 0xe4, 0xe4, 0xa7, 0x17, 0x4b, 0x30, 0x83, 0x02,
-	0x30, 0x08, 0xcc, 0x56, 0x4a, 0xe0, 0xe2, 0x07, 0x3b, 0xc1, 0x27, 0x35, 0xb1, 0x8c, 0x60, 0x6c,
-	0x23, 0xc7, 0x02, 0x13, 0x91, 0xb1, 0xe0, 0x64, 0x77, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72,
-	0x8c, 0x0f, 0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7,
-	0x72, 0x0c, 0x51, 0x2a, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xd0, 0xf4,
-	0x0c, 0xa5, 0x74, 0x8b, 0x53, 0xb2, 0xf5, 0x2b, 0x20, 0xc9, 0x39, 0x89, 0x0d, 0x9c, 0x8c, 0x8d,
-	0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x5b, 0xc0, 0x98, 0xf8, 0x2f, 0x03, 0x00, 0x00,
+	// 442 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x93, 0x4f, 0xef, 0xd2, 0x30,
+	0x18, 0xc7, 0xe9, 0x4f, 0x02, 0x52, 0x8c, 0x98, 0xfa, 0x27, 0x03, 0xc9, 0x20, 0xc4, 0x44, 0x0e,
+	0xb2, 0x05, 0x4c, 0xd4, 0x93, 0x44, 0x0c, 0x31, 0x24, 0x1c, 0xc8, 0xf0, 0x4f, 0xe2, 0x05, 0xc7,
+	0xda, 0x8c, 0xc5, 0x41, 0x97, 0xb6, 0x9b, 0x70, 0xf4, 0x1d, 0xf8, 0x52, 0x3c, 0xf8, 0x22, 0x3c,
+	0x12, 0x4f, 0x1e, 0x0d, 0xbc, 0x11, 0xb3, 0xae, 0x03, 0x82, 0x31, 0x23, 0xf9, 0x9d, 0x68, 0xfb,
+	0xfd, 0x7c, 0xbf, 0x3c, 0x4f, 0x9f, 0x15, 0xd6, 0x1d, 0xca, 0x97, 0x94, 0x9b, 0x2e, 0xa3, 0x61,
+	0x60, 0x46, 0x5d, 0x93, 0x44, 0x64, 0x25, 0xb8, 0x11, 0x30, 0x2a, 0x28, 0xaa, 0x24, 0xaa, 0x21,
+	0x55, 0x23, 0xea, 0xd6, 0xaa, 0xc9, 0xc1, 0x4c, 0xca, 0xa6, 0x52, 0xe5, 0xa6, 0xf6, 0xf0, 0x3c,
+	0x49, 0x6c, 0x02, 0xa2, 0xc4, 0x56, 0x07, 0xde, 0x19, 0xc6, 0xc1, 0xaf, 0x19, 0xb1, 0x05, 0x79,
+	0x13, 0x23, 0xa8, 0x0a, 0x6f, 0x4a, 0x76, 0xe6, 0x61, 0x0d, 0x34, 0x41, 0x3b, 0x6f, 0x15, 0xe5,
+	0x7e, 0x84, 0x0f, 0xf8, 0xbb, 0x00, 0x5f, 0x82, 0x8f, 0xe1, 0x83, 0xf3, 0xf4, 0x09, 0xf5, 0x3d,
+	0x67, 0x83, 0x7a, 0xb0, 0x68, 0x63, 0xcc, 0x08, 0xe7, 0xd2, 0x53, 0x1a, 0x68, 0xbf, 0x7e, 0x74,
+	0xee, 0xa9, 0xba, 0x5f, 0x25, 0xca, 0x54, 0x30, 0x6f, 0xe5, 0x5a, 0x29, 0x78, 0x48, 0x3b, 0xf9,
+	0xf3, 0x6b, 0xa4, 0x3d, 0x83, 0x77, 0x65, 0xda, 0x34, 0x9c, 0x2f, 0x3d, 0x31, 0x61, 0x34, 0xa0,
+	0xdc, 0xf6, 0x51, 0x03, 0x96, 0x03, 0xb5, 0x3e, 0x36, 0x04, 0xd3, 0xa3, 0x11, 0x6e, 0xbd, 0x80,
+	0xf7, 0xa5, 0xef, 0x83, 0x27, 0x16, 0x98, 0xd9, 0x5f, 0x2e, 0x77, 0x3e, 0x81, 0x25, 0xe9, 0x7c,
+	0x4f, 0x05, 0xc9, 0xa6, 0xbf, 0x02, 0x85, 0x0f, 0xd7, 0xc4, 0xc9, 0xc4, 0x51, 0x1f, 0x16, 0x18,
+	0xe1, 0xa1, 0x2f, 0xb4, 0xab, 0x26, 0x68, 0xdf, 0xee, 0x3d, 0x36, 0xce, 0x3e, 0x11, 0x23, 0x2d,
+	0x34, 0xce, 0x0b, 0x05, 0x65, 0x96, 0xc4, 0x2d, 0x65, 0x43, 0x08, 0xe6, 0x7d, 0xea, 0x72, 0xed,
+	0x46, 0x7c, 0x81, 0x96, 0x5c, 0xb7, 0x3e, 0xc1, 0x8a, 0x2c, 0x61, 0x4c, 0xec, 0x28, 0x73, 0xda,
+	0xa7, 0x53, 0xb8, 0xba, 0x74, 0x0a, 0xdf, 0x81, 0x1a, 0x43, 0x5a, 0xdd, 0x84, 0x85, 0x2b, 0x82,
+	0xb3, 0xfb, 0x7d, 0x0e, 0x0b, 0x5c, 0xd8, 0x22, 0xe4, 0xaa, 0xdf, 0xc6, 0x7f, 0xfb, 0x9d, 0x4a,
+	0xcc, 0x52, 0x38, 0xea, 0xc3, 0x5b, 0xc2, 0xf6, 0xfd, 0xcd, 0x4c, 0x5d, 0x57, 0xdc, 0x6f, 0xb9,
+	0x57, 0xff, 0xc7, 0xfe, 0x36, 0x86, 0xd4, 0x1d, 0x95, 0xc5, 0x71, 0x33, 0x78, 0xf9, 0x73, 0xa7,
+	0x83, 0xed, 0x4e, 0x07, 0x7f, 0x76, 0x3a, 0xf8, 0xb6, 0xd7, 0x73, 0xdb, 0xbd, 0x9e, 0xfb, 0xbd,
+	0xd7, 0x73, 0x1f, 0x1f, 0xb9, 0x9e, 0x58, 0x84, 0x73, 0xc3, 0xa1, 0x4b, 0xf5, 0x04, 0xd5, 0x4f,
+	0x87, 0xe3, 0xcf, 0xe6, 0x3a, 0x79, 0x81, 0xf3, 0x82, 0x7c, 0x79, 0x4f, 0xff, 0x06, 0x00, 0x00,
+	0xff, 0xff, 0xa5, 0x1a, 0x1c, 0xb9, 0xe2, 0x03, 0x00, 0x00,
 }
 
 func (m *EventCreateGroup) Marshal() (dAtA []byte, err error) {
@@ -781,6 +850,51 @@ func (m *EventLeaveGroup) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *EventProposalPruned) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *EventProposalPruned) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *EventProposalPruned) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.TallyResult != nil {
+		{
+			size, err := m.TallyResult.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintEvents(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x1a
+	}
+	if m.Status != 0 {
+		i = encodeVarintEvents(dAtA, i, uint64(m.Status))
+		i--
+		dAtA[i] = 0x10
+	}
+	if m.ProposalId != 0 {
+		i = encodeVarintEvents(dAtA, i, uint64(m.ProposalId))
+		i--
+		dAtA[i] = 0x8
+	}
+	return len(dAtA) - i, nil
+}
+
 func encodeVarintEvents(dAtA []byte, offset int, v uint64) int {
 	offset -= sovEvents(v)
 	base := offset
@@ -913,6 +1027,25 @@ func (m *EventLeaveGroup) Size() (n int) {
 	return n
 }
 
+func (m *EventProposalPruned) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.ProposalId != 0 {
+		n += 1 + sovEvents(uint64(m.ProposalId))
+	}
+	if m.Status != 0 {
+		n += 1 + sovEvents(uint64(m.Status))
+	}
+	if m.TallyResult != nil {
+		l = m.TallyResult.Size()
+		n += 1 + l + sovEvents(uint64(l))
+	}
+	return n
+}
+
 func sovEvents(x uint64) (n int) {
 	return (math_bits.Len64(x|1) + 6) / 7
 }
@@ -1649,6 +1782,130 @@ func (m *EventLeaveGroup) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *EventProposalPruned) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowEvents
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: EventProposalPruned: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: EventProposalPruned: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ProposalId", wireType)
+			}
+			m.ProposalId = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowEvents
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.ProposalId |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType)
+			}
+			m.Status = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowEvents
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Status |= ProposalStatus(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field TallyResult", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowEvents
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthEvents
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthEvents
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.TallyResult == nil {
+				m.TallyResult = &TallyResult{}
+			}
+			if err := m.TallyResult.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipEvents(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthEvents
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func skipEvents(dAtA []byte) (n int, err error) {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/x/group/keeper/genesis.go b/x/group/keeper/genesis.go
index fd4eb13c004f..e1664b41acc0 100644
--- a/x/group/keeper/genesis.go
+++ b/x/group/keeper/genesis.go
@@ -44,7 +44,7 @@ func (k Keeper) InitGenesis(ctx types.Context, cdc codec.JSONCodec, data json.Ra
 }
 
 // ExportGenesis returns the group module's exported genesis.
-func (k Keeper) ExportGenesis(ctx types.Context, cdc codec.JSONCodec) *group.GenesisState {
+func (k Keeper) ExportGenesis(ctx types.Context, _ codec.JSONCodec) *group.GenesisState {
 	genesisState := group.NewGenesisState()
 
 	var groups []*group.GroupInfo
diff --git a/x/group/keeper/keeper.go b/x/group/keeper/keeper.go
index f8f41673cef8..1cce70e3eb25 100644
--- a/x/group/keeper/keeper.go
+++ b/x/group/keeper/keeper.go
@@ -374,6 +374,15 @@ func (k Keeper) PruneProposals(ctx sdk.Context) error {
 		if err != nil {
 			return err
 		}
+		// Emit event for proposal finalized with its result
+		if err := ctx.EventManager().EmitTypedEvent(
+			&group.EventProposalPruned{
+				ProposalId:  proposal.Id,
+				Status:      proposal.Status,
+				TallyResult: &proposal.FinalTallyResult,
+			}); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -407,6 +416,14 @@ func (k Keeper) TallyProposalsAtVPEnd(ctx sdk.Context) error {
 			if err := k.pruneVotes(ctx, proposalID); err != nil {
 				return err
 			}
+			// Emit event for proposal finalized with its result
+			if err := ctx.EventManager().EmitTypedEvent(
+				&group.EventProposalPruned{
+					ProposalId: proposal.Id,
+					Status:     proposal.Status,
+				}); err != nil {
+				return err
+			}
 		} else if proposal.Status == group.PROPOSAL_STATUS_SUBMITTED {
 			if err := k.doTallyAndUpdate(ctx, &proposal, electorate, policyInfo); err != nil {
 				return sdkerrors.Wrap(err, "doTallyAndUpdate")
diff --git a/x/group/keeper/keeper_test.go b/x/group/keeper/keeper_test.go
index 0bd2027b83c4..1e3b09e0c144 100644
--- a/x/group/keeper/keeper_test.go
+++ b/x/group/keeper/keeper_test.go
@@ -10,6 +10,7 @@ import (
 	"testing"
 	"time"
 
+	abci "github.com/cometbft/cometbft/abci/types"
 	"github.com/cometbft/cometbft/libs/log"
 	tmtime "github.com/cometbft/cometbft/types/time"
 	"github.com/golang/mock/gomock"
@@ -32,6 +33,8 @@ import (
 	minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
 )
 
+var EventProposalPruned = "cosmos.group.v1.EventProposalPruned"
+
 const minExecutionPeriod = 5 * time.Second
 
 type TestSuite struct {
@@ -1756,6 +1759,9 @@ func (s *TestSuite) TestSubmitProposal() {
 				s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900))
 				toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2)
 				s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100))
+
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
 			},
 		},
 		"with try exec, not enough yes votes for proposal to pass": {
@@ -1847,6 +1853,7 @@ func (s *TestSuite) TestWithdrawProposal() {
 		proposalId uint64
 		admin      string
 		expErrMsg  string
+		postRun    func(sdkCtx sdk.Context)
 	}{
 		"wrong admin": {
 			preRun: func(sdkCtx sdk.Context) uint64 {
@@ -1854,6 +1861,7 @@ func (s *TestSuite) TestWithdrawProposal() {
 			},
 			admin:     addr5.String(),
 			expErrMsg: "unauthorized",
+			postRun:   func(sdkCtx sdk.Context) {},
 		},
 		"wrong proposalId": {
 			preRun: func(sdkCtx sdk.Context) uint64 {
@@ -1861,6 +1869,7 @@ func (s *TestSuite) TestWithdrawProposal() {
 			},
 			admin:     proposers[0],
 			expErrMsg: "not found",
+			postRun:   func(sdkCtx sdk.Context) {},
 		},
 		"happy case with proposer": {
 			preRun: func(sdkCtx sdk.Context) uint64 {
@@ -1868,6 +1877,7 @@ func (s *TestSuite) TestWithdrawProposal() {
 			},
 			proposalId: proposalID,
 			admin:      proposers[0],
+			postRun:    func(sdkCtx sdk.Context) {},
 		},
 		"already closed proposal": {
 			preRun: func(sdkCtx sdk.Context) uint64 {
@@ -1882,6 +1892,7 @@ func (s *TestSuite) TestWithdrawProposal() {
 			proposalId: proposalID,
 			admin:      proposers[0],
 			expErrMsg:  "cannot withdraw a proposal with the status of PROPOSAL_STATUS_WITHDRAWN",
+			postRun:    func(sdkCtx sdk.Context) {},
 		},
 		"happy case with group admin address": {
 			preRun: func(sdkCtx sdk.Context) uint64 {
@@ -1889,6 +1900,17 @@ func (s *TestSuite) TestWithdrawProposal() {
 			},
 			proposalId: proposalID,
 			admin:      proposers[0],
+			postRun: func(sdkCtx sdk.Context) {
+				resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: proposalID})
+				s.Require().NoError(err)
+				vpe := resp.Proposal.VotingPeriodEnd
+				timeDiff := vpe.Sub(s.sdkCtx.BlockTime())
+				ctxVPE := sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(timeDiff).Add(time.Second * 1))
+				s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctxVPE))
+				events := ctxVPE.EventManager().ABCIEvents()
+
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
+			},
 		},
 	}
 	for msg, spec := range specs {
@@ -1911,6 +1933,7 @@ func (s *TestSuite) TestWithdrawProposal() {
 			resp, err := s.groupKeeper.Proposal(s.ctx, &group.QueryProposalRequest{ProposalId: pId})
 			s.Require().NoError(err)
 			s.Require().Equal(resp.GetProposal().Status, group.PROPOSAL_STATUS_WITHDRAWN)
+			spec.postRun(s.sdkCtx)
 		})
 	}
 }
@@ -2425,6 +2448,7 @@ func (s *TestSuite) TestExecProposal() {
 		expBalance        bool
 		expFromBalances   sdk.Coin
 		expToBalances     sdk.Coin
+		postRun           func(sdkCtx sdk.Context)
 	}{
 		"proposal executed when accepted": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2438,6 +2462,10 @@ func (s *TestSuite) TestExecProposal() {
 			expBalance:        true,
 			expFromBalances:   sdk.NewInt64Coin("test", 9900),
 			expToBalances:     sdk.NewInt64Coin("test", 100),
+			postRun: func(sdkCtx sdk.Context) {
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
+			},
 		},
 		"proposal with multiple messages executed when accepted": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2452,6 +2480,10 @@ func (s *TestSuite) TestExecProposal() {
 			expBalance:        true,
 			expFromBalances:   sdk.NewInt64Coin("test", 9800),
 			expToBalances:     sdk.NewInt64Coin("test", 200),
+			postRun: func(sdkCtx sdk.Context) {
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
+			},
 		},
 		"proposal not executed when rejected": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2461,6 +2493,7 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
 			expProposalStatus: group.PROPOSAL_STATUS_REJECTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
+			postRun:           func(sdkCtx sdk.Context) {},
 		},
 		"open proposal must not fail": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2468,12 +2501,14 @@ func (s *TestSuite) TestExecProposal() {
 			},
 			expProposalStatus: group.PROPOSAL_STATUS_SUBMITTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
+			postRun:           func(sdkCtx sdk.Context) {},
 		},
 		"existing proposal required": {
 			setupProposal: func(ctx context.Context) uint64 {
 				return 9999
 			},
-			expErr: true,
+			expErr:  true,
+			postRun: func(sdkCtx sdk.Context) {},
 		},
 		"Decision policy also applied on exactly voting period end": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2483,6 +2518,7 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(time.Second), // Voting period is 1s
 			expProposalStatus: group.PROPOSAL_STATUS_REJECTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
+			postRun:           func(sdkCtx sdk.Context) {},
 		},
 		"Decision policy also applied after voting period end": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2492,6 +2528,7 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(time.Second).Add(time.Millisecond), // Voting period is 1s
 			expProposalStatus: group.PROPOSAL_STATUS_REJECTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
+			postRun:           func(sdkCtx sdk.Context) {},
 		},
 		"exec proposal before MinExecutionPeriod should fail": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2501,6 +2538,7 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(4 * time.Second), // min execution date is 5s later after s.blockTime
 			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, // Because MinExecutionPeriod has not passed
+			postRun:           func(sdkCtx sdk.Context) {},
 		},
 		"exec proposal at exactly MinExecutionPeriod should pass": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2511,6 +2549,10 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(5 * time.Second), // min execution date is 5s later after s.blockTime
 			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
+			postRun: func(sdkCtx sdk.Context) {
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
+			},
 		},
 		"prevent double execution when successful": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2533,6 +2575,10 @@ func (s *TestSuite) TestExecProposal() {
 			expBalance:        true,
 			expFromBalances:   sdk.NewInt64Coin("test", 9900),
 			expToBalances:     sdk.NewInt64Coin("test", 100),
+			postRun: func(sdkCtx sdk.Context) {
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
+			},
 		},
 		"rollback all msg updates on failure": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2545,6 +2591,7 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
 			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE,
+			postRun:           func(sdkCtx sdk.Context) {},
 		},
 		"executable when failed before": {
 			setupProposal: func(ctx context.Context) uint64 {
@@ -2568,6 +2615,10 @@ func (s *TestSuite) TestExecProposal() {
 			srcBlockTime:      s.blockTime.Add(minExecutionPeriod), // After min execution period end
 			expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED,
 			expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
+			postRun: func(sdkCtx sdk.Context) {
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
+			},
 		},
 	}
 	for msg, spec := range specs {
@@ -2614,6 +2665,8 @@ func (s *TestSuite) TestExecProposal() {
 				toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2)
 				s.Require().Contains(toBalances, spec.expToBalances)
 			}
+
+			spec.postRun(sdkCtx)
 		})
 	}
 }
@@ -2802,6 +2855,8 @@ func (s *TestSuite) TestExecPrunedProposalsAndVotes() {
 				s.Require().NoError(err)
 				s.Require().Empty(res.GetVotes())
 
+				events := sdkCtx.EventManager().ABCIEvents()
+				s.Require().True(eventTypeFound(events, EventProposalPruned))
 			} else {
 				// Check that proposal and votes exists
 				res, err := s.groupKeeper.Proposal(ctx, &group.QueryProposalRequest{ProposalId: proposalID})
@@ -3376,3 +3431,14 @@ func (s *TestSuite) TestTallyProposalsAtVPEnd_GroupMemberLeaving() {
 	s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctx))
 	s.NotPanics(func() { module.EndBlocker(ctx, s.groupKeeper) })
 }
+
+func eventTypeFound(events []abci.Event, eventType string) bool {
+	eventTypeFound := false
+	for _, e := range events {
+		if e.Type == eventType {
+			eventTypeFound = true
+			break
+		}
+	}
+	return eventTypeFound
+}
diff --git a/x/group/keeper/msg_server.go b/x/group/keeper/msg_server.go
index 0112a98083e0..348c27c89285 100644
--- a/x/group/keeper/msg_server.go
+++ b/x/group/keeper/msg_server.go
@@ -715,6 +715,7 @@ func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate
 		} else {
 			p.Status = group.PROPOSAL_STATUS_REJECTED
 		}
+
 	}
 
 	return nil
@@ -786,6 +787,16 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR
 		if err := k.pruneProposal(ctx, proposal.Id); err != nil {
 			return nil, err
 		}
+
+		// Emit event for proposal finalized with its result
+		if err := ctx.EventManager().EmitTypedEvent(
+			&group.EventProposalPruned{
+				ProposalId:  proposal.Id,
+				Status:      proposal.Status,
+				TallyResult: &proposal.FinalTallyResult,
+			}); err != nil {
+			return nil, err
+		}
 	} else {
 		store := ctx.KVStore(k.key)
 		if err := k.proposalTable.Update(store, id, &proposal); err != nil {
diff --git a/x/group/types.pb.go b/x/group/types.pb.go
index 882d8e42893c..f375179d8d4b 100644
--- a/x/group/types.pb.go
+++ b/x/group/types.pb.go
@@ -642,6 +642,8 @@ type GroupPolicyInfo struct {
 	// admin is the account address of the group admin.
 	Admin string `protobuf:"bytes,3,opt,name=admin,proto3" json:"admin,omitempty"`
 	// metadata is any arbitrary metadata attached to the group policy.
+	// the recommended format of the metadata is to be found here:
+	// https://docs.cosmos.network/v0.47/modules/group#decision-policy-1
 	Metadata string `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
 	// version is used to track changes to a group's GroupPolicyInfo structure that
 	// would create a different result on a running proposal.
@@ -695,6 +697,8 @@ type Proposal struct {
 	// group_policy_address is the account address of group policy.
 	GroupPolicyAddress string `protobuf:"bytes,2,opt,name=group_policy_address,json=groupPolicyAddress,proto3" json:"group_policy_address,omitempty"`
 	// metadata is any arbitrary metadata attached to the proposal.
+	// the recommended format of the metadata is to be found here:
+	// https://docs.cosmos.network/v0.47/modules/group#proposal-4
 	Metadata string `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"`
 	// proposers are the account addresses of the proposers.
 	Proposers []string `protobuf:"bytes,4,rep,name=proposers,proto3" json:"proposers,omitempty"`