diff --git a/pkg/action/action.go b/pkg/action/action.go
new file mode 100644
index 0000000000000000000000000000000000000000..4233d986f5cf0ecd199f64c42b5f6453e0533fce
--- /dev/null
+++ b/pkg/action/action.go
@@ -0,0 +1,40 @@
+package action
+
+import (
+	"net/url"
+	"strings"
+)
+
+// URL структура для хранения данных о переданном действии.
+type URL struct {
+	*url.URL
+}
+
+// NewURL возвращает структуру ActionURL
+func NewURL(action string) (*URL, error) {
+	u, err := url.Parse(action)
+	if err != nil {
+		return nil, err
+	}
+	return &URL{URL: u}, nil
+}
+
+func (u *URL) actionParts() (string, string) {
+	if u.URL != nil && u.URL.Scheme == "grpc" {
+		splitPath := strings.Split(strings.TrimLeft(u.Path, "/"), "/")
+		if len(splitPath) >= 2 {
+			return splitPath[0], splitPath[1]
+		}
+	}
+	return "", ""
+}
+
+func (u *URL) Action() string {
+	_, action := u.actionParts()
+	return action
+}
+
+func (u *URL) Extension() string {
+	ext, _ := u.actionParts()
+	return ext
+}
diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..2988744f4b88b734c4085b5aa20ceb95fe0657da
--- /dev/null
+++ b/pkg/action/action_test.go
@@ -0,0 +1,135 @@
+package action
+
+import (
+	"fmt"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestActionURL_New(t *testing.T) {
+	tests := []struct {
+		name    string
+		action  string
+		want    *URL
+		url     string
+		wantErr assert.ErrorAssertionFunc
+	}{
+		{
+			name: "Without action",
+			want: &URL{
+				URL: &url.URL{},
+			},
+			wantErr: assert.NoError,
+		},
+		{
+			name:   "Without deprecated action call",
+			action: "build-site",
+			want: &URL{
+				URL: &url.URL{
+					Path: "build-site",
+				},
+			},
+			url:     "build-site",
+			wantErr: assert.NoError,
+		},
+		{
+			name:   "With grpc action",
+			action: "grpc:///perxisweb/build-site",
+			want: &URL{
+				URL: &url.URL{
+					Scheme: "grpc",
+					Path:   "/perxisweb/build-site",
+				},
+			},
+			url:     "grpc:///perxisweb/build-site",
+			wantErr: assert.NoError,
+		},
+		{
+			name:   "With ui action",
+			action: "ui:///space/env/coll",
+			want: &URL{
+				URL: &url.URL{
+					Scheme: "ui",
+					Path:   "/space/env/coll",
+				},
+			},
+			url:     "ui:///space/env/coll",
+			wantErr: assert.NoError,
+		},
+		{
+			name:   "With http action",
+			action: "https://perx.ru",
+			want: &URL{
+				URL: &url.URL{
+					Scheme: "https",
+					Host:   "perx.ru",
+				},
+			},
+			url:     "https://perx.ru",
+			wantErr: assert.NoError,
+		},
+		{
+			name:    "With error in parse",
+			action:  "grpc://user:abc{DEf1=ghi@example.com:5432/db?sslmode=require",
+			want:    nil,
+			wantErr: assert.Error,
+		},
+		{
+			name:   "With no action id",
+			action: "grpc:///perxisweb",
+			want: &URL{
+				URL: &url.URL{
+					Scheme: "grpc",
+					Path:   "/perxisweb",
+				},
+			},
+			wantErr: assert.NoError,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := NewURL(tt.action)
+			if !tt.wantErr(t, err, fmt.Sprintf("NewURL(%v)", tt.action)) {
+				return
+			}
+			assert.Equalf(t, tt.want, got, "NewURL(%v)", tt.action)
+		})
+	}
+}
+
+func TestActionURL_String(t *testing.T) {
+	tests := []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "GRPC action",
+			url:  "grpc:///perxisweb/build-site",
+		},
+		{
+			name: "UI action #1",
+			url:  "ui:///space/env/coll",
+		},
+		{
+			name: "UI action deprecated call #2",
+			url:  "space/env/coll",
+		},
+		{
+			name: "Https action",
+			url:  "https://perx.ru",
+		},
+		{
+			name: "With action deprecated call",
+			url:  "extension-id",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p, _ := NewURL(tt.url)
+			assert.Equalf(t, tt.url, p.String(), "String()")
+		})
+	}
+}
diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go
index 70bd6f05e6f3e5c422c88e193a7e1621eaa2f9e6..989454178f640144791a9e6fd84d39317fd1e283 100644
--- a/pkg/expr/mongo.go
+++ b/pkg/expr/mongo.go
@@ -57,16 +57,11 @@ func convertToMongo(ctx context.Context, tree *parser.Tree, env map[string]inter
 	}
 
 	c := &compiler{tree: tree, env: env, config: config, identifierRenameFn: identifierRenameFn}
-	v := c.compile(tree.Node)
-	switch e := v.(type) {
-	case bson.M:
-		b = e
-	case string:
-		b = bson.M{"$text": bson.M{"$search": e}}
-	default:
-		err = fmt.Errorf("invalid expression")
+	v, ok := c.compile(tree.Node).(bson.M)
+	if !ok || v == nil {
+		return nil, fmt.Errorf("invalid expression")
 	}
-	return
+	return v, nil
 }
 
 type compiler struct {
diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go
index 46f92b551086ae7ad1d5336f35c98d3154b4253d..75ec627c124cc0a24768d3cec30a4eac0ae34c15 100644
--- a/pkg/expr/mongo_test.go
+++ b/pkg/expr/mongo_test.go
@@ -50,10 +50,15 @@ func TestConvertToMongo(t *testing.T) {
 		{"time", fmt.Sprintf("d > Time.Time('%s')", now.Format(time.RFC3339)), nil, bson.M{"d": bson.M{"$gt": tm}}, false},
 		{"in", "In(s, [1,2,3])", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false},
 		{"in", "In(s, 1)", nil, bson.M{"s": bson.M{"$in": []interface{}{1}}}, false},
+		{"text search or id", "id", nil, nil, true},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			gotB, err := ConvertToMongo(ctx, tt.eval, tt.env, nil)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
 			require.NoError(t, err)
 			assert.Equal(t, tt.wantB, gotB)
 		})
@@ -72,7 +77,7 @@ func BenchmarkConvertToMongo(b *testing.B) {
 	//fmt.Println(len(exp))
 
 	for i := 0; i < b.N; i++ {
-		ConvertToMongo(ctx, exp, nil, nil, expr.Patch(&testVisitor{}))
+		_, _ = ConvertToMongo(ctx, exp, nil, nil, expr.Patch(&testVisitor{}))
 	}
 }
 
diff --git a/pkg/extension/server.go b/pkg/extension/server.go
index f683d0914d897de21dc052f93734c6604e69ced9..f9b603f50ffb6109f89551a7e3b92b5c355761d4 100644
--- a/pkg/extension/server.go
+++ b/pkg/extension/server.go
@@ -3,6 +3,7 @@ package extension
 import (
 	"context"
 
+	"git.perx.ru/perxis/perxis-go/pkg/action"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
 )
@@ -80,8 +81,19 @@ func (srv *Server) Update(ctx context.Context, request *UpdateRequest) (*UpdateR
 }
 
 func (srv *Server) Action(ctx context.Context, in *pb.ActionRequest) (*pb.ActionResponse, error) {
+	actionURL, err := action.NewURL(in.Action)
+	if err != nil {
+		return nil, err
+	}
+	ext := actionURL.Extension()
+	if ext == "" {
+		ext = in.Extension
+	}
+	if ext == "" {
+		return nil, errors.New("extension ID required")
+	}
 
-	svc, ok := srv.services[in.Extension]
+	svc, ok := srv.services[ext]
 	if !ok {
 		return nil, ErrUnknownExtension
 	}
diff --git a/pkg/extension/server_test.go b/pkg/extension/server_test.go
index 5400c8c4786cbe09bf2c56fdfb0611d65048d31d..bee68b1a8e3f8ca90beae259ceda2a933e3f6b3e 100644
--- a/pkg/extension/server_test.go
+++ b/pkg/extension/server_test.go
@@ -2,11 +2,13 @@ package extension
 
 import (
 	"context"
+	"fmt"
 	"reflect"
 	"strings"
 	"testing"
 
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestGetResults(t *testing.T) {
@@ -102,3 +104,102 @@ func (t testServerExtension) Uninstall(ctx context.Context, in *UninstallRequest
 func (t testServerExtension) Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) {
 	return &ActionResponse{}, t.err
 }
+
+func TestServer_Action(t *testing.T) {
+	getDummyExtension := func(name string, wantErr ...bool) Extension {
+		ext := &testServerExtension{name: name}
+
+		if len(wantErr) > 0 {
+			ext.err = errors.WithDetail(errors.New("some err"), "Ошибка")
+		}
+
+		return ext
+	}
+
+	var tests = []struct {
+		name     string
+		services map[string]Extension
+		in       *ActionRequest
+		want     *ActionResponse
+		wantErr  string
+	}{
+		{
+			name:     "GRPC",
+			services: map[string]Extension{"test-extension": getDummyExtension("test-extension")},
+			in: &ActionRequest{
+				Action:  "grpc:///test-extension/test-action",
+				SpaceId: "sp",
+				EnvId:   "env",
+			},
+			want: &ActionResponse{State: ResponseDone},
+		},
+		{
+			name:     "invalid schema",
+			services: map[string]Extension{"test-extension": getDummyExtension("test-extension")},
+			in: &ActionRequest{
+				Action:  "some:///space/env/coll",
+				SpaceId: "sp",
+				EnvId:   "env",
+			},
+			want:    &ActionResponse{State: ResponseDone},
+			wantErr: "extension ID required",
+		},
+		{
+			name:     "Deprecated call",
+			services: map[string]Extension{"test-extension": getDummyExtension("test-extension")},
+			in: &ActionRequest{
+				Action:    "test-action",
+				SpaceId:   "sp",
+				EnvId:     "env",
+				Extension: "test-extension",
+			},
+			want: &ActionResponse{State: ResponseDone},
+		},
+		{
+			name:     "unknown extension",
+			services: map[string]Extension{"test-extension": getDummyExtension("test-extension")},
+			in: &ActionRequest{
+				Action:  "grpc:///test-extension-2/test-action",
+				SpaceId: "sp",
+				EnvId:   "env",
+			},
+			want:    nil,
+			wantErr: ErrUnknownExtension.Error(),
+		},
+		{
+			name:     "Deprecated call, without extension",
+			services: map[string]Extension{"test-extension": getDummyExtension("test-extension")},
+			in: &ActionRequest{
+				Action:  "test-action",
+				SpaceId: "sp",
+				EnvId:   "env",
+			},
+			want:    nil,
+			wantErr: "extension ID required",
+		},
+		{
+			name:     "Deprecated call, without action and extension)",
+			services: map[string]Extension{"test-extension": getDummyExtension("test-extension")},
+			in: &ActionRequest{
+				SpaceId: "sp",
+				EnvId:   "env",
+			},
+			want:    nil,
+			wantErr: "extension ID required",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			srv := &Server{
+				services: tt.services,
+			}
+			got, err := srv.Action(context.Background(), tt.in)
+			if tt.wantErr != "" {
+				assert.EqualErrorf(t, err, tt.wantErr, fmt.Sprintf("Action(%v)", tt.in))
+				return
+			}
+			assert.Equalf(t, tt.want, got, "Action(%v)", tt.in)
+		})
+	}
+}
diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go
index 1ee716d9c1d8066b2e1c5a6adbd082382ae30dc0..6d9608ab6877acdc94bfcad0bd62e5fa21eef181 100644
--- a/pkg/extension/service/extension.go
+++ b/pkg/extension/service/extension.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 
+	"git.perx.ru/perxis/perxis-go/pkg/action"
 	"git.perx.ru/perxis/perxis-go/pkg/clients"
 	"git.perx.ru/perxis/perxis-go/pkg/content"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
@@ -155,7 +156,18 @@ func (s *Extension) Uninstall(ctx context.Context, in *extension.UninstallReques
 }
 
 func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) {
-	ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, in.Extension)
+	actionURL, err := action.NewURL(in.Action)
+	if err != nil {
+		return nil, err
+	}
+	ext := actionURL.Extension()
+	if ext == "" {
+		ext = in.Extension
+	}
+	if ext == "" {
+		return nil, errors.New("extension ID required")
+	}
+	ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, ext)
 	if err != nil {
 		return nil, errors.Wrap(err, "check extension installed")
 	}
diff --git a/pkg/items/service.go b/pkg/items/service.go
index 6a550dd0ad642f2dd1e02f7966b5963bf2147979..1f4f2867ebfc78ef5bc32bc68c4cd9371acd84bb 100644
--- a/pkg/items/service.go
+++ b/pkg/items/service.go
@@ -52,8 +52,9 @@ type PreSaver interface {
 }
 
 type Filter struct {
-	ID     []string
-	Data   []*filter.Filter
+	ID   []string
+	Data []*filter.Filter
+	// DEPRECATED Use Q instead
 	Search string // Поиск, одновременно поддерживается только один запрос
 	Q      []string
 }
diff --git a/pkg/schema/validate/string.go b/pkg/schema/validate/string.go
index f05ca540034e3cfab7e57c1aa324bcb343b97431..33fa453d0c707b893803f6e43484e50f232e8506 100644
--- a/pkg/schema/validate/string.go
+++ b/pkg/schema/validate/string.go
@@ -18,6 +18,9 @@ func MaxLength(max int) Validator {
 }
 
 func (t maxLength) Validate(_ context.Context, field *field.Field, value interface{}) error {
+	if value == nil {
+		return nil
+	}
 	if s, ok := value.(string); ok {
 		n := utf8.RuneCountInString(s)
 		if t > 0 && n > int(t) {
@@ -36,6 +39,9 @@ func MinLength(max int) Validator {
 }
 
 func (t minLength) Validate(_ context.Context, field *field.Field, value interface{}) error {
+	if value == nil {
+		return nil
+	}
 	if s, ok := value.(string); ok {
 		n := utf8.RuneCountInString(s)
 		if n < int(t) {
diff --git a/pkg/schema/validate/string_test.go b/pkg/schema/validate/string_test.go
index b67e8c8d4f5056c61d5483a536701777d716bead..9d48cf4d75c5c7bba2763c125bc1b598e78b23ac 100644
--- a/pkg/schema/validate/string_test.go
+++ b/pkg/schema/validate/string_test.go
@@ -92,9 +92,15 @@ func TestString(t *testing.T) {
 		wantErr bool
 	}{
 		{"Length Max", field.String().AddOptions(MaxLength(5)), "1234567", true},
+		{"Length Max with <nil>", field.String().AddOptions(MaxLength(5)), nil, false},
+		{"Length Max and Required with <nil>", field.String().AddOptions(MaxLength(5), Required()), nil, true},
 		{"Length Min", field.String().AddOptions(MinLength(10)), "1234", true},
+		{"Length Min with <nil>", field.String().AddOptions(MinLength(10)), nil, false},
+		{"Length Min and Required with <nil>", field.String().AddOptions(MinLength(10), Required()), nil, true},
 		{"Length MinMax", field.String().AddOptions(MaxLength(6), MinLength(2)), "1234567", true},
 		{"Length MinMax", field.String().AddOptions(MaxLength(10), MinLength(7)), "123456", true},
+		{"Length MinMax with <nil>", field.String().AddOptions(MaxLength(10), MinLength(7)), nil, false},
+		{"Length MinMax and Required with <nil>", field.String().AddOptions(MaxLength(10), MinLength(7), Required()), nil, true},
 		{"Enum miss", field.String().AddOptions(Enum(EnumOpt{Name: "N 1", Value: "n1"}, EnumOpt{Name: "N 2", Value: "n2"})), "n3", true},
 		{"Enum match", field.String().AddOptions(Enum(EnumOpt{Name: "N 1", Value: "n1"}, EnumOpt{Name: "N 2", Value: "n2"})), "n2", false},
 		{"Invalid Schema Options", field.String().AddOptions(Schema()), invalidOptionsSchema, true},
@@ -129,8 +135,6 @@ func TestStringValidate(t *testing.T) {
 	}{
 		{"String Length Max", field.String().AddOptions(MaxLength(1)), "1", false, ""},
 		{"String Length Min", field.String().AddOptions(MinLength(1)), "1", false, ""},
-		{"Nil Length Max", field.String().AddOptions(MaxLength(1)), nil, true, "validation error: incorrect type: \"invalid\", expected \"string\""},
-		{"Nil Length Min", field.String().AddOptions(MinLength(1)), nil, true, "validation error: incorrect type: \"invalid\", expected \"string\""},
 		{"Int Length Max", field.String().AddOptions(MaxLength(1)), 1, true, "validation error: incorrect type: \"int\", expected \"string\""},
 		{"Int Length Min", field.String().AddOptions(MinLength(1)), 1, true, "validation error: incorrect type: \"int\", expected \"string\""},
 		{"Float Length Max", field.String().AddOptions(MaxLength(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"string\""},