code: func ExtractProperties(s *Step) (*structpb.Struct, error) {
  	finals := map[string]json.RawMessage{}
  
  	var extract func(s *Step)
  	extract = func(s *Step) {
  		for _, p := range s.Property {
  			finals[p.Name] = json.RawMessage(p.Value)
  		}
  		for _, substep := range s.GetSubstep() {
  			if ss := substep.GetStep(); ss != nil {
  				extract(ss)
  			}
  		}
  	}
  	extract(s)
  
  	buf, err := json.Marshal(finals)
  	if err != nil {
  		panic("failed to marshal in-memory map to JSON")
  	}
  
  	ret := &structpb.Struct{}
  	if err := jsonpb.Unmarshal(bytes.NewReader(buf), ret); err != nil {
  		return nil, err
  	}
  	return ret, nil
  }
docs: // ExtractProperties returns a flat list of properties from a tree of steps.
  // If multiple step nodes have a property of the same name, the last one in the
  // preorder traversal wins.