diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts
index ad79749ae3..6208ed9284 100644
--- a/models/recruit/src/index.ts
+++ b/models/recruit/src/index.ts
@@ -452,7 +452,7 @@ export function createModel (builder: Builder): void {
   })
 
   builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ClassFilters, {
-    filters: ['title', 'source', 'city', 'modifiedOn', 'onsite', 'remote']
+    filters: ['title', 'source', 'city', 'skills', 'modifiedOn', 'onsite', 'remote']
   })
 
   builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, {
diff --git a/models/server-tags/src/index.ts b/models/server-tags/src/index.ts
index 01c1f5aadd..e939fe228d 100644
--- a/models/server-tags/src/index.ts
+++ b/models/server-tags/src/index.ts
@@ -20,6 +20,10 @@ import serverTags from '@anticrm/server-tags'
 import tags from '@anticrm/tags'
 
 export function createModel (builder: Builder): void {
+  builder.createDoc(serverCore.class.Trigger, core.space.Model, {
+    trigger: serverTags.trigger.onTagReference
+  })
+
   builder.mixin<Class<Doc>, ObjectDDParticipant>(
     tags.class.TagElement,
     core.class.Class,
diff --git a/models/tags/src/index.ts b/models/tags/src/index.ts
index 5e5c8d0d76..2a4598a883 100644
--- a/models/tags/src/index.ts
+++ b/models/tags/src/index.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import { Class, Doc, Domain, IndexKind, Ref } from '@anticrm/core'
-import { ArrOf, Builder, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
+import { ArrOf, Builder, Index, Model, Prop, TypeNumber, TypeRef, TypeString, UX } from '@anticrm/model'
 import core, { TAttachedDoc, TDoc } from '@anticrm/model-core'
 import view from '@anticrm/model-view'
 import { Asset, IntlString } from '@anticrm/platform'
@@ -45,6 +45,9 @@ export class TTagElement extends TDoc implements TagElement {
 
   @Prop(TypeRef(tags.class.TagCategory), tags.string.CategoryLabel)
   category!: Ref<TagCategory>
+
+  @Prop(TypeNumber(), tags.string.TagReference)
+  refCount?: number
 }
 
 @Model(tags.class.TagReference, core.class.AttachedDoc, DOMAIN_TAGS)
@@ -94,4 +97,8 @@ export function createModel (builder: Builder): void {
   builder.mixin(tags.class.TagElement, core.class.Class, view.mixin.AttributePresenter, {
     presenter: tags.component.TagElementPresenter
   })
+
+  builder.mixin(tags.class.TagReference, core.class.Class, view.mixin.AttributeFilter, {
+    component: tags.component.TagsFilter
+  })
 }
diff --git a/models/tags/src/migration.ts b/models/tags/src/migration.ts
index a2869993cb..d1f84168a1 100644
--- a/models/tags/src/migration.ts
+++ b/models/tags/src/migration.ts
@@ -1,8 +1,36 @@
-import core, { TxOperations } from '@anticrm/core'
-import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
+import core, { Ref, TxOperations } from '@anticrm/core'
+import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model'
+import { TagElement, TagReference } from '@anticrm/tags'
 import { DOMAIN_TAGS } from './index'
 import tags from './plugin'
 
+async function updateTagRefCount (client: MigrationClient): Promise<void> {
+  const tagElements = await client.find(DOMAIN_TAGS, { _class: tags.class.TagElement, refCount: { $exists: false } })
+  const refs = await client.find<TagReference>(DOMAIN_TAGS, {
+    _class: tags.class.TagReference,
+    tag: { $in: tagElements.map((p) => p._id as Ref<TagElement>) }
+  })
+  const map = new Map<Ref<TagElement>, number>()
+  for (const ref of refs) {
+    map.set(ref.tag, (map.get(ref.tag) ?? 0) + 1)
+  }
+  const promises: Promise<MigrationResult>[] = []
+  for (const tag of map) {
+    promises.push(
+      client.update(
+        DOMAIN_TAGS,
+        {
+          _id: tag[0]
+        },
+        {
+          refCount: tag[1]
+        }
+      )
+    )
+  }
+  await Promise.all(promises)
+}
+
 export const tagsOperation: MigrateOperation = {
   async migrate (client: MigrationClient): Promise<void> {
     await client.update(
@@ -15,6 +43,8 @@ export const tagsOperation: MigrateOperation = {
         category: 'recruit:category:Other'
       }
     )
+
+    await updateTagRefCount(client)
   },
   async upgrade (client: MigrationUpgradeClient): Promise<void> {
     const tx = new TxOperations(client, core.account.System)
diff --git a/models/tags/src/plugin.ts b/models/tags/src/plugin.ts
index 175ab2af54..b5f3ef0a7c 100644
--- a/models/tags/src/plugin.ts
+++ b/models/tags/src/plugin.ts
@@ -25,7 +25,8 @@ export default mergeIds(tagsId, tags, {
     Tags: '' as AnyComponent,
     TagReferencePresenter: '' as AnyComponent,
     TagsPresenter: '' as AnyComponent,
-    TagsItemPresenter: '' as AnyComponent
+    TagsItemPresenter: '' as AnyComponent,
+    TagsFilter: '' as AnyComponent
   },
   string: {
     TagElementLabel: '' as IntlString,
diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts
index 92f5877ef6..ae89796555 100644
--- a/packages/presentation/src/utils.ts
+++ b/packages/presentation/src/utils.ts
@@ -78,11 +78,13 @@ export class LiveQuery {
   private oldCallback: ((result: FindResult<any>) => void) | undefined
   unsubscribe = () => {}
 
-  constructor () {
-    onDestroy(() => {
-      console.log('onDestroy query')
-      this.unsubscribe()
-    })
+  constructor (dontDestroy: boolean = false) {
+    if (!dontDestroy) {
+      onDestroy(() => {
+        console.log('onDestroy query')
+        this.unsubscribe()
+      })
+    }
   }
 
   query<T extends Doc>(
@@ -121,8 +123,8 @@ export class LiveQuery {
   }
 }
 
-export function createQuery (): LiveQuery {
-  return new LiveQuery()
+export function createQuery (dontDestroy?: boolean): LiveQuery {
+  return new LiveQuery(dontDestroy)
 }
 
 export function getFileUrl (file: string): string {
diff --git a/plugins/recruit-resources/src/components/Candidates.svelte b/plugins/recruit-resources/src/components/Candidates.svelte
index 8293ade2ed..b4c7f0b138 100644
--- a/plugins/recruit-resources/src/components/Candidates.svelte
+++ b/plugins/recruit-resources/src/components/Candidates.svelte
@@ -15,10 +15,9 @@
 -->
 <script lang="ts">
   import contact from '@anticrm/contact'
-  import { Doc, DocumentQuery, Ref } from '@anticrm/core'
+  import { Doc, DocumentQuery } from '@anticrm/core'
   import { createQuery, getClient } from '@anticrm/presentation'
-  import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
-  import { ActionIcon, showPopup, Component, Icon, Label, Loading, SearchEdit, Button, IconAdd } from '@anticrm/ui'
+  import { ActionIcon, showPopup, Icon, Label, Loading, SearchEdit, Button, IconAdd } from '@anticrm/ui'
   import view, { Viewlet, ViewletPreference } from '@anticrm/view'
   import { ActionContext, TableBrowser, ViewletSetting } from '@anticrm/view-resources'
   import recruit from '../plugin'
@@ -57,29 +56,11 @@
       }
     })
 
-  let category: Ref<TagCategory> | undefined = undefined
-
-  let documentIds: Ref<Doc>[] = []
-  async function updateResultQuery (search: string, documentIds: Ref<Doc>[]): Promise<void> {
+  async function updateResultQuery (search: string): Promise<void> {
     resultQuery = search === '' ? {} : { $search: search }
-    if (documentIds.length > 0) {
-      resultQuery._id = { $in: documentIds }
-    }
   }
 
-  // Find all tags for object classe with matched elements
-  const query = createQuery()
-
-  $: query.query(tags.class.TagReference, { tag: { $in: $selectedTagElements } }, (result) => {
-    documentIds = Array.from(new Set<Ref<Doc>>(result.map((it) => it.attachedTo)).values())
-  })
-
-  $: updateResultQuery(search, documentIds)
-
-  function updateCategory (detail: { category: Ref<TagCategory> | null; elements: TagElement[] }) {
-    category = detail.category ?? undefined
-    selectedTagElements.set(Array.from(detail.elements ?? []).map((it) => it._id))
-  }
+  $: updateResultQuery(search)
 
   function showCreateDialog () {
     showPopup(CreateCandidate, {}, 'top')
@@ -95,7 +76,7 @@
   <SearchEdit
     bind:value={search}
     on:change={() => {
-      updateResultQuery(search, documentIds)
+      updateResultQuery(search)
     }}
   />
   <Button icon={IconAdd} label={recruit.string.CandidateCreateLabel} kind={'primary'} on:click={showCreateDialog} />
@@ -111,12 +92,6 @@
   {/if}
 </div>
 
-<Component
-  is={tags.component.TagsCategoryBar}
-  props={{ targetClass: recruit.mixin.Candidate, category, selected: $selectedTagElements, mode: 'item' }}
-  on:change={(evt) => updateCategory(evt.detail)}
-/>
-
 <ActionContext
   context={{
     mode: 'browser'
diff --git a/plugins/tags-resources/package.json b/plugins/tags-resources/package.json
index d879dff392..2739505601 100644
--- a/plugins/tags-resources/package.json
+++ b/plugins/tags-resources/package.json
@@ -37,6 +37,7 @@
     "@anticrm/ui": "~0.6.0",
     "@anticrm/presentation": "~0.6.2",
     "@anticrm/core": "~0.6.16",
+    "@anticrm/view": "~0.6.0",
     "@anticrm/view-resources": "~0.6.0"
   }
 }
diff --git a/plugins/tags-resources/src/components/TagsFilter.svelte b/plugins/tags-resources/src/components/TagsFilter.svelte
new file mode 100644
index 0000000000..67214bf192
--- /dev/null
+++ b/plugins/tags-resources/src/components/TagsFilter.svelte
@@ -0,0 +1,229 @@
+<!--
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+-->
+<script context="module" lang="ts">
+  const liveQueries: Map<number, LiveQuery> = new Map<number, LiveQuery>()
+  const results: Map<number, Ref<Doc>[]> = new Map<number, Ref<Doc>[]>()
+</script>
+
+<script lang="ts">
+  import { Class, Doc, Ref } from '@anticrm/core'
+  import { translate } from '@anticrm/platform'
+  import presentation, { createQuery, getClient, LiveQuery } from '@anticrm/presentation'
+  import { Button, CheckBox, getPlatformColor } from '@anticrm/ui'
+  import { Filter } from '@anticrm/view'
+  import view from '@anticrm/view-resources/src/plugin'
+  import { createEventDispatcher, onMount } from 'svelte'
+  import tags from '../plugin'
+  import { TagCategory, TagElement } from '@anticrm/tags'
+
+  export let _class: Ref<Class<Doc>>
+  export let filter: Filter
+  export let onChange: (e: Filter) => void
+  filter.onRemove = () => {
+    const lq = liveQueries.get(filter.index)
+    lq?.unsubscribe()
+    liveQueries.delete(filter.index)
+    results.delete(filter.index)
+  }
+  const lq = getLiveQuery(filter.index)
+  const client = getClient()
+  let selected: Ref<TagElement>[] = filter.value
+
+  function getLiveQuery (index: number): LiveQuery {
+    let lq = liveQueries.get(index)
+    if (lq === undefined) {
+      lq = createQuery(true)
+      liveQueries.set(index, lq)
+    }
+    return lq
+  }
+
+  async function getRefs (res: Ref<TagElement>[], onUpdate: () => void): Promise<Ref<Doc>[]> {
+    const promise = new Promise<Ref<Doc>[]>((resolve, reject) => {
+      const refresh = lq.query(
+        tags.class.TagReference,
+        {
+          tag: { $in: res }
+        },
+        (refs) => {
+          const result = Array.from(new Set(refs.map((p) => p.attachedTo)))
+          results.set(filter.index, result)
+          resolve(result)
+          onUpdate()
+        }
+      )
+
+      if (!refresh) {
+        resolve(results.get(filter.index) ?? [])
+      }
+    })
+    return promise
+  }
+
+  filter.modes = [
+    {
+      label: view.string.FilterIs,
+      isAvailable: (res: any[]) => res.length <= 1,
+      result: async (res: any[], onUpdate: () => void) => {
+        const result = await getRefs(res, onUpdate)
+        return { $in: result }
+      }
+    },
+    {
+      label: view.string.FilterIsEither,
+      isAvailable: (res: any[]) => res.length > 1,
+      result: async (res: any[], onUpdate: () => void) => {
+        const result = await getRefs(res, onUpdate)
+        return { $in: result }
+      }
+    },
+    {
+      label: view.string.FilterIsNot,
+      isAvailable: () => true,
+      result: async (res: any[], onUpdate: () => void) => {
+        const result = await getRefs(res, onUpdate)
+        return { $nin: result }
+      }
+    }
+  ]
+
+  let categories: TagCategory[] = []
+  let objects: TagElement[] = []
+  client.findAll(tags.class.TagCategory, { targetClass: _class }).then((res) => {
+    categories = res
+  })
+
+  $: getValues(search)
+
+  async function getValues (search: string): Promise<void> {
+    const resultQuery =
+      search !== ''
+        ? {
+            title: { $like: '%' + search + '%' },
+            targetClass: _class
+          }
+        : { targetClass: _class }
+    objects = await client.findAll(tags.class.TagElement, resultQuery)
+  }
+
+  function checkMode () {
+    if (filter.mode?.isAvailable(filter.value)) return
+    const newMode = filter.modes.find((p: any) => p.isAvailable(filter.value))
+    filter.mode = newMode !== undefined ? newMode : filter.mode
+  }
+
+  let search: string = ''
+  let phTraslate: string = ''
+  let searchInput: HTMLInputElement
+  $: translate(presentation.string.Search, {}).then((res) => {
+    phTraslate = res
+  })
+
+  onMount(() => {
+    if (searchInput) searchInput.focus()
+  })
+
+  const toggleGroup = (ev: MouseEvent): void => {
+    const el: HTMLElement = ev.currentTarget as HTMLElement
+    el.classList.toggle('show')
+  }
+  const show: boolean = false
+
+  const getCount = (cat: TagCategory): string => {
+    const count = objects.filter((el) => el.category === cat._id).filter((it) => selected.includes(it._id)).length
+    if (count > 0) return count.toString()
+    return ''
+  }
+
+  const isSelected = (element: TagElement): boolean => {
+    if (selected.filter((p) => p === element._id).length > 0) return true
+    return false
+  }
+
+  const checkSelected = (element: TagElement): void => {
+    if (isSelected(element)) {
+      selected = selected.filter((p) => p !== element._id)
+    } else {
+      selected = [...selected, element._id]
+    }
+    objects = objects
+    categories = categories
+  }
+
+  const dispatch = createEventDispatcher()
+</script>
+
+<div class="selectPopup">
+  <div class="header">
+    <input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
+  </div>
+  <div class="scroll">
+    <div class="box">
+      {#each categories as cat}
+        {#if objects.filter((el) => el.category === cat._id).length > 0}
+          <div class="sticky-wrapper">
+            <button class="menu-group__header" class:show={search !== '' || show} on:click={toggleGroup}>
+              <div class="flex-row-center">
+                <span class="mr-1-5">{cat.label}</span>
+                <div class="icon">
+                  <svg fill="var(--content-color)" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
+                    <path d="M0,0L6,3L0,6Z" />
+                  </svg>
+                </div>
+              </div>
+              <div class="flex-row-center text-xs">
+                <span class="content-color mr-1">({objects.filter((el) => el.category === cat._id).length})</span>
+                <span class="counter">{getCount(cat)}</span>
+              </div>
+            </button>
+            <div class="menu-group">
+              {#each objects.filter((el) => el.category === cat._id) as element}
+                <button
+                  class="menu-item"
+                  on:click={() => {
+                    checkSelected(element)
+                  }}
+                >
+                  <div class="flex-between w-full">
+                    <div class="flex">
+                      <div class="check pointer-events-none">
+                        <CheckBox checked={isSelected(element)} primary />
+                      </div>
+                      <div class="tag" style="background-color: {getPlatformColor(element.color)};" />
+                      {element.title}
+                    </div>
+                    <div class="content-trans-color ml-2">
+                      {element.refCount ?? 0}
+                    </div>
+                  </div>
+                </button>
+              {/each}
+            </div>
+          </div>
+        {/if}
+      {/each}
+    </div>
+  </div>
+  <Button
+    shape={'round'}
+    label={view.string.Apply}
+    on:click={async () => {
+      filter.value = selected
+      checkMode()
+      onChange(filter)
+      dispatch('close')
+    }}
+  />
+</div>
diff --git a/plugins/tags-resources/src/index.ts b/plugins/tags-resources/src/index.ts
index 6a081699d2..0c3c960f0b 100644
--- a/plugins/tags-resources/src/index.ts
+++ b/plugins/tags-resources/src/index.ts
@@ -26,6 +26,7 @@ import TagsItemPresenter from './components/TagsItemPresenter.svelte'
 import TagsPresenter from './components/TagsPresenter.svelte'
 import TagsView from './components/TagsView.svelte'
 import TagElementCountPresenter from './components/TagElementCountPresenter.svelte'
+import TagsFilter from './components/TagsFilter.svelte'
 
 export default async (): Promise<Resources> => ({
   component: {
@@ -34,6 +35,7 @@ export default async (): Promise<Resources> => ({
     TagElementPresenter,
     TagsPresenter,
     TagsView,
+    TagsFilter,
     TagsEditor,
     TagsDropdownEditor,
     TagsItemPresenter,
diff --git a/plugins/tags/src/index.ts b/plugins/tags/src/index.ts
index 89bf390238..4d6ef2b5dc 100644
--- a/plugins/tags/src/index.ts
+++ b/plugins/tags/src/index.ts
@@ -28,6 +28,7 @@ export interface TagElement extends Doc {
   description: string
   color: number
   category: Ref<TagCategory>
+  refCount?: number
 }
 
 /**
diff --git a/plugins/view-assets/lang/en.json b/plugins/view-assets/lang/en.json
index 3cded693be..86ddfd4430 100644
--- a/plugins/view-assets/lang/en.json
+++ b/plugins/view-assets/lang/en.json
@@ -45,6 +45,7 @@
     "FilterIsEither": "is either of",
     "FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
     "Before": "Before",
-    "After": "After"
+    "After": "After",
+    "Apply": "Apply"
   }
 }
diff --git a/plugins/view-assets/lang/ru.json b/plugins/view-assets/lang/ru.json
index 9bfa86a8d3..24f2dac41e 100644
--- a/plugins/view-assets/lang/ru.json
+++ b/plugins/view-assets/lang/ru.json
@@ -43,6 +43,7 @@
     "FilterIsEither": "один из",
     "FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}",
     "Before": "До",
-    "After": "После"
+    "After": "После",
+    "Apply": "Применить"
   }
 }
diff --git a/plugins/view-resources/src/components/filter/FilterBar.svelte b/plugins/view-resources/src/components/filter/FilterBar.svelte
index cc3c5e0a40..6a5f7542b1 100644
--- a/plugins/view-resources/src/components/filter/FilterBar.svelte
+++ b/plugins/view-resources/src/components/filter/FilterBar.svelte
@@ -30,29 +30,29 @@
   const dispatch = createEventDispatcher()
 
   let filters: Filter[] = []
-  let isNew = true
+  let maxIndex = 0
 
   function onChange (e: Filter | undefined) {
     if (e === undefined) return
-    if (isNew) {
+    const index = filters.findIndex((p) => p.index === e.index)
+    if (index === -1) {
       filters.push(e)
-      isNew = false
       filters = filters
     } else {
-      filters[filters.length - 1] = e
+      filters[index] = e
       filters = filters
     }
   }
 
   function add (e: MouseEvent) {
     const target = eventToHTMLElement(e)
-    isNew = true
     showPopup(
       FilterTypePopup,
       {
         _class,
-        makeQuery: (key: string) => makeQuery(query, filters, key),
+        query,
         target,
+        index: ++maxIndex,
         onChange
       },
       target
@@ -60,19 +60,21 @@
   }
 
   function remove (i: number) {
+    filters[i]?.onRemove?.()
     filters.splice(i, 1)
     filters = filters
   }
 
-  function makeQuery (query: DocumentQuery<Doc>, filters: Filter[], skipKey?: string): DocumentQuery<Doc> {
+  async function makeQuery (query: DocumentQuery<Doc>, filters: Filter[]): Promise<void> {
     const newQuery = hierarchy.clone(query)
     for (let i = 0; i < filters.length; i++) {
       const filter = filters[i]
-      if (skipKey !== undefined && filter.key.key === skipKey) continue
+      const newValue = await filter.mode.result(filter.value, () => {
+        makeQuery(query, filters)
+      })
       if (newQuery[filter.key.key] === undefined) {
-        newQuery[filter.key.key] = filter.mode.result(filter.value)
+        newQuery[filter.key.key] = newValue
       } else {
-        const newValue = filter.mode.result(filter.value)
         let merged = false
         for (const key in newValue) {
           if (newQuery[filter.key.key][key] === undefined) {
@@ -104,14 +106,11 @@
           }
         }
         if (!merged) {
-          Object.assign(newQuery[filter.key.key], filter.mode.result(filter.value))
+          Object.assign(newQuery[filter.key.key], newValue)
         }
       }
     }
-    if (skipKey === undefined) {
-      dispatch('change', newQuery)
-    }
-    return newQuery
+    dispatch('change', newQuery)
   }
 
   $: makeQuery(query, filters)
@@ -125,7 +124,7 @@
     {#each filters as filter, i}
       <FilterSection
         {_class}
-        query={makeQuery(query, filters, filter.key.key)}
+        {query}
         {filter}
         on:change={() => {
           makeQuery(query, filters)
diff --git a/plugins/view-resources/src/components/filter/FilterTypePopup.svelte b/plugins/view-resources/src/components/filter/FilterTypePopup.svelte
index c669fab5c7..1c11d02821 100644
--- a/plugins/view-resources/src/components/filter/FilterTypePopup.svelte
+++ b/plugins/view-resources/src/components/filter/FilterTypePopup.svelte
@@ -13,7 +13,17 @@
 // limitations under the License.
 -->
 <script lang="ts">
-  import core, { AnyAttribute, ArrOf, Class, Doc, DocumentQuery, Ref, Type } from '@anticrm/core'
+  import core, {
+    AnyAttribute,
+    ArrOf,
+    AttachedDoc,
+    Class,
+    Collection,
+    Doc,
+    DocumentQuery,
+    Ref,
+    Type
+  } from '@anticrm/core'
   import { getClient } from '@anticrm/presentation'
   import { Icon, Label, showPopup } from '@anticrm/ui'
   import { Filter, KeyFilter } from '@anticrm/view'
@@ -21,8 +31,9 @@
   import view from '../../plugin'
 
   export let _class: Ref<Class<Doc>>
-  export let makeQuery: (key: string) => DocumentQuery<Doc>
+  export let query: DocumentQuery<Doc>
   export let target: HTMLElement
+  export let index: number
   export let onChange: (e: Filter) => void
 
   const client = getClient()
@@ -48,11 +59,13 @@
   }
 
   function buildFilter (key: string, attribute: AnyAttribute): KeyFilter | undefined {
-    const clazz = hierarchy.getClass(attribute.type._class)
+    const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
+    const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
+    const clazz = hierarchy.getClass(targetClass)
     const filter = hierarchy.as(clazz, view.mixin.AttributeFilter)
     if (filter.component === undefined) return undefined
     return {
-      key: key,
+      key: isCollection ? '_id' : key,
       label: attribute.label,
       icon: attribute.icon,
       component: filter.component
@@ -108,10 +121,11 @@
       type.component,
       {
         _class,
-        query: makeQuery(type.key),
+        query,
         filter: {
           key: type,
-          value: []
+          value: [],
+          index
         },
         onChange
       },
diff --git a/plugins/view-resources/src/components/filter/ObjectFilter.svelte b/plugins/view-resources/src/components/filter/ObjectFilter.svelte
index faa153cea7..4fb5096235 100644
--- a/plugins/view-resources/src/components/filter/ObjectFilter.svelte
+++ b/plugins/view-resources/src/components/filter/ObjectFilter.svelte
@@ -16,11 +16,12 @@
   import { Class, Doc, DocumentQuery, getObjectValue, Ref, RefTo } from '@anticrm/core'
   import { translate } from '@anticrm/platform'
   import presentation, { getClient } from '@anticrm/presentation'
-  import ui, { CheckBox, Label } from '@anticrm/ui'
+  import ui, { Button, CheckBox, Label } from '@anticrm/ui'
   import { Filter } from '@anticrm/view'
   import { onMount } from 'svelte'
   import { buildConfigLookup, getPresenter } from '../../utils'
   import view from '../../plugin'
+  import { createEventDispatcher } from 'svelte'
 
   export let _class: Ref<Class<Doc>>
   export let query: DocumentQuery<Doc>
@@ -37,21 +38,21 @@
     {
       label: view.string.FilterIs,
       isAvailable: (res: any[]) => res.length <= 1,
-      result: (res: any[]) => {
+      result: async (res: any[]) => {
         return { $in: res }
       }
     },
     {
       label: view.string.FilterIsEither,
       isAvailable: (res: any[]) => res.length > 1,
-      result: (res: any[]) => {
+      result: async (res: any[]) => {
         return { $in: res }
       }
     },
     {
       label: view.string.FilterIsNot,
       isAvailable: () => true,
-      result: (res: any[]) => {
+      result: async (res: any[]) => {
         return { $nin: res }
       }
     }
@@ -107,7 +108,6 @@
       }
     }
     checkMode()
-    onChange(filter)
   }
 
   let search: string = ''
@@ -120,18 +120,13 @@
   onMount(() => {
     if (searchInput) searchInput.focus()
   })
+
+  const dispatch = createEventDispatcher()
 </script>
 
 <div class="selectPopup">
   <div class="header">
-    <input
-      bind:this={searchInput}
-      type="text"
-      bind:value={search}
-      placeholder={phTraslate}
-      on:input={(ev) => {}}
-      on:change
-    />
+    <input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
   </div>
   <div class="scroll">
     <div class="box">
@@ -154,7 +149,7 @@
                   <Label label={ui.string.NotSelected} />
                 {/if}
               </div>
-              <div class="content-trans-color">
+              <div class="content-trans-color ml-2">
                 {targets.get(value?._id)}
               </div>
             </div>
@@ -163,4 +158,12 @@
       {/await}
     </div>
   </div>
+  <Button
+    shape={'round'}
+    label={view.string.Apply}
+    on:click={() => {
+      onChange(filter)
+      dispatch('close')
+    }}
+  />
 </div>
diff --git a/plugins/view-resources/src/components/filter/TimestampFilter.svelte b/plugins/view-resources/src/components/filter/TimestampFilter.svelte
index 3ce8d89d2e..7f0a49a34c 100644
--- a/plugins/view-resources/src/components/filter/TimestampFilter.svelte
+++ b/plugins/view-resources/src/components/filter/TimestampFilter.svelte
@@ -27,14 +27,14 @@
     {
       label: view.string.Before,
       isAvailable: (res: any[]) => true,
-      result: (res: any[]) => {
+      result: async (res: any[]) => {
         return { $lt: res[0] }
       }
     },
     {
       label: view.string.After,
       isAvailable: () => true,
-      result: (res: any[]) => {
+      result: async (res: any[]) => {
         return { $gt: res[0] }
       }
     }
diff --git a/plugins/view-resources/src/components/filter/ValueFilter.svelte b/plugins/view-resources/src/components/filter/ValueFilter.svelte
index 6f5f08d74a..b07439127f 100644
--- a/plugins/view-resources/src/components/filter/ValueFilter.svelte
+++ b/plugins/view-resources/src/components/filter/ValueFilter.svelte
@@ -16,11 +16,12 @@
   import { Class, Doc, DocumentQuery, getObjectValue, Ref } from '@anticrm/core'
   import { translate } from '@anticrm/platform'
   import presentation, { getClient } from '@anticrm/presentation'
-  import ui, { CheckBox, Label } from '@anticrm/ui'
+  import ui, { Button, CheckBox, Label } from '@anticrm/ui'
   import { Filter } from '@anticrm/view'
   import { onMount } from 'svelte'
   import { getPresenter } from '../../utils'
   import view from '../../plugin'
+  import { createEventDispatcher } from 'svelte'
 
   export let _class: Ref<Class<Doc>>
   export let query: DocumentQuery<Doc>
@@ -31,22 +32,22 @@
     {
       label: view.string.FilterIs,
       isAvailable: (res: any[]) => res.length <= 1,
-      result: (res: any[]) => {
-        return { $in: res }
+      result: async (res: [any, any[]][]) => {
+        return { $in: res.map((p) => p[1]).flat() }
       }
     },
     {
       label: view.string.FilterIsEither,
       isAvailable: (res: any[]) => res.length > 1,
-      result: (res: any[]) => {
-        return { $in: res }
+      result: async (res: [any, any[]][]) => {
+        return { $in: res.map((p) => p[1]).flat() }
       }
     },
     {
       label: view.string.FilterIsNot,
       isAvailable: () => true,
-      result: (res: any[]) => {
-        return { $nin: res }
+      result: async (res: [any, any[]][]) => {
+        return { $nin: res.map((p) => p[1]).flat() }
       }
     }
   ]
@@ -56,7 +57,7 @@
   const promise = getPresenter(client, _class, key, key)
 
   let values = new Map<any, number>()
-  let selectedValues: Set<any> = new Set<any>()
+  let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))
   const realValues = new Map<any, Set<any>>()
 
   $: getValues(search)
@@ -98,11 +99,6 @@
       selectedValues.add(value)
     }
     selectedValues = selectedValues
-    filter.value = Array.from(selectedValues.values())
-      .map((p) => Array.from(realValues.get(p) ?? []))
-      .flat()
-    checkMode()
-    onChange(filter)
   }
 
   let search: string = ''
@@ -112,6 +108,8 @@
     phTraslate = res
   })
 
+  const dispatch = createEventDispatcher()
+
   onMount(() => {
     if (searchInput) searchInput.focus()
   })
@@ -119,14 +117,7 @@
 
 <div class="selectPopup">
   <div class="header">
-    <input
-      bind:this={searchInput}
-      type="text"
-      bind:value={search}
-      placeholder={phTraslate}
-      on:input={(ev) => {}}
-      on:change
-    />
+    <input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
   </div>
   <div class="scroll">
     <div class="box">
@@ -149,7 +140,7 @@
                   <Label label={ui.string.NotSelected} />
                 {/if}
               </div>
-              <div class="content-trans-color">
+              <div class="content-trans-color ml-2">
                 {values.get(value)}
               </div>
             </div>
@@ -158,4 +149,16 @@
       {/await}
     </div>
   </div>
+  <Button
+    shape={'round'}
+    label={view.string.Apply}
+    on:click={() => {
+      filter.value = Array.from(selectedValues.values()).map((p) => {
+        return [p, Array.from(realValues.get(p) ?? [])]
+      })
+      checkMode()
+      onChange(filter)
+      dispatch('close')
+    }}
+  />
 </div>
diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts
index 66abde5fa2..d9d3bf69a0 100644
--- a/plugins/view-resources/src/plugin.ts
+++ b/plugins/view-resources/src/plugin.ts
@@ -48,6 +48,7 @@ export default mergeIds(viewId, view, {
     FilterIsEither: '' as IntlString,
     FilterStatesCount: '' as IntlString,
     Before: '' as IntlString,
-    After: '' as IntlString
+    After: '' as IntlString,
+    Apply: '' as IntlString
   }
 })
diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts
index e191c7ac55..bcc9abc2d8 100644
--- a/plugins/view/src/index.ts
+++ b/plugins/view/src/index.ts
@@ -52,7 +52,7 @@ export interface KeyFilter {
 export interface FilterMode {
   label: IntlString
   isAvailable: (values: any[]) => boolean
-  result: (values: any[]) => ObjQueryType<any>
+  result: (values: any[], onUpdate: () => void) => Promise<ObjQueryType<any>>
 }
 
 /**
@@ -63,6 +63,8 @@ export interface Filter {
   mode: FilterMode
   modes: FilterMode[]
   value: any[]
+  index: number
+  onRemove?: () => void
 }
 
 /**
diff --git a/server-plugins/tags-resources/src/index.ts b/server-plugins/tags-resources/src/index.ts
index 80ba025b0f..bec338f513 100644
--- a/server-plugins/tags-resources/src/index.ts
+++ b/server-plugins/tags-resources/src/index.ts
@@ -13,27 +13,95 @@
 // limitations under the License.
 //
 
-import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } from '@anticrm/core'
-import tags, { TagElement } from '@anticrm/tags'
+import core, {
+  AttachedDoc,
+  Class,
+  Doc,
+  DocumentQuery,
+  FindOptions,
+  FindResult,
+  Hierarchy,
+  Ref,
+  Tx,
+  TxCollectionCUD,
+  TxCreateDoc,
+  TxCUD,
+  TxProcessor,
+  TxRemoveDoc
+} from '@anticrm/core'
+import { TriggerControl } from '@anticrm/server-core'
+import tags, { TagElement, TagReference } from '@anticrm/tags'
+
+const extractTx = (tx: Tx): Tx => {
+  if (tx._class === core.class.TxCollectionCUD) {
+    const ctx = tx as TxCollectionCUD<Doc, AttachedDoc>
+    if (ctx.tx._class === core.class.TxCreateDoc) {
+      const create = ctx.tx as TxCreateDoc<AttachedDoc>
+      create.attributes.attachedTo = ctx.objectId
+      create.attributes.attachedToClass = ctx.objectClass
+      create.attributes.collection = ctx.collection
+      return create
+    }
+    return ctx.tx
+  }
+
+  return tx
+}
 
 /**
  * @public
  */
 export async function TagElementRemove (
   doc: Doc,
-  hiearachy: Hierarchy,
+  hierarchy: Hierarchy,
   findAll: <T extends Doc>(
     clazz: Ref<Class<T>>,
     query: DocumentQuery<T>,
     options?: FindOptions<T>
   ) => Promise<FindResult<T>>
 ): Promise<Doc[]> {
-  if (!hiearachy.isDerived(doc._class, tags.class.TagElement)) return []
+  if (!hierarchy.isDerived(doc._class, tags.class.TagElement)) return []
   return await findAll(tags.class.TagReference, { tag: doc._id as Ref<TagElement> })
 }
 
+/**
+ * @public
+ */
+export async function onTagReference (tx: Tx, control: TriggerControl): Promise<Tx[]> {
+  const actualTx = extractTx(tx)
+  const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
+  const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)
+  if (!isCreate && !isRemove) return []
+  if (!control.hierarchy.isDerived((actualTx as TxCUD<Doc>).objectClass, tags.class.TagReference)) return []
+  if (isCreate) {
+    const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<TagReference>)
+    const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, tags.space.Tags, doc.tag, {
+      $inc: { refCount: 1 }
+    })
+    return [res]
+  }
+  if (isRemove) {
+    const ctx = actualTx as TxRemoveDoc<TagReference>
+    const createTx = (
+      await control.findAll(core.class.TxCollectionCUD, { 'tx.objectId': ctx.objectId }, { limit: 1 })
+    )[0]
+    if (createTx !== undefined) {
+      const actualCreateTx = extractTx(createTx)
+      const doc = TxProcessor.createDoc2Doc(actualCreateTx as TxCreateDoc<TagReference>)
+      const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, tags.space.Tags, doc.tag, {
+        $inc: { refCount: -1 }
+      })
+      return [res]
+    }
+  }
+  return []
+}
+
 // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
 export default async () => ({
+  trigger: {
+    onTagReference
+  },
   function: {
     TagElementRemove
   }
diff --git a/server-plugins/tags/src/index.ts b/server-plugins/tags/src/index.ts
index 377d299d95..632055da5c 100644
--- a/server-plugins/tags/src/index.ts
+++ b/server-plugins/tags/src/index.ts
@@ -16,6 +16,7 @@
 import { Class, Doc, Hierarchy, Ref, FindResult, FindOptions, DocumentQuery } from '@anticrm/core'
 import type { Plugin, Resource } from '@anticrm/platform'
 import { plugin } from '@anticrm/platform'
+import { TriggerFunc } from '@anticrm/server-core'
 
 /**
  * @public
@@ -26,6 +27,9 @@ export const serverTagsId = 'server-tags' as Plugin
  * @public
  */
 export default plugin(serverTagsId, {
+  trigger: {
+    onTagReference: '' as Resource<TriggerFunc>
+  },
   function: {
     TagElementRemove: '' as Resource<
     (